Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flutter replay for Android #2032

Merged
merged 102 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from 84 commits
Commits
Show all changes
102 commits
Select commit Hold shift + click to select a range
294317b
minor gradle fixes
vaind Apr 3, 2024
861ebab
tmp: local sentry-java build
vaind Apr 3, 2024
2f55619
tmp: use relative path to sentry-java
vaind Apr 4, 2024
209ca41
tmp: local java build patches
vaind Apr 4, 2024
a282fad
replay options
vaind Apr 10, 2024
7d8d4e8
replay recorder
vaind Apr 11, 2024
c887e6d
Merge branch 'main' into feat/replay
vaind Apr 11, 2024
3444961
wip: JNI native bindings
vaind Apr 11, 2024
ca03c95
use compatible jnigen
vaind Apr 17, 2024
0b06dd5
add missing gradlew to flutter/android
vaind Apr 17, 2024
314665d
Merge branch 'feat/jni-ffi' into feat/replay
vaind Apr 17, 2024
8bf52d8
replay recorder JNI binding code
vaind Apr 17, 2024
a4e056a
replay recorder binding jni code
vaind Apr 17, 2024
1cde833
jni 0.6
vaind Apr 18, 2024
010f575
wip: android jni replay
vaind Apr 18, 2024
4c4f132
replay binding
vaind Apr 18, 2024
87d18e7
glue code for jni
vaind Apr 22, 2024
2eaaa28
Merge branch 'main' into feat/replay
vaind Apr 23, 2024
afa9f50
chore: update to cocoa 8.24.1-alpha.0
vaind Apr 23, 2024
3df2523
wip: cocoa integration
vaind Apr 24, 2024
179b5d4
wip: ios replay
vaind Apr 24, 2024
9284db8
Merge branch 'main' into feat/replay
vaind Apr 25, 2024
6432b98
cleanup
vaind Apr 25, 2024
25fd690
formatting
vaind Apr 25, 2024
658a132
android fixes
vaind Apr 26, 2024
fb8bbb4
move native setup to the native sdk integration
vaind Apr 26, 2024
7c8fd42
cleanup & improvements
vaind Apr 29, 2024
0570f35
improve widget filter and implement redact options
vaind Apr 29, 2024
5c08b21
fix image scaling
vaind Apr 29, 2024
c00788a
Merge branch 'main' into feat/replay
vaind May 2, 2024
0c55dc6
ktlint format
vaind May 2, 2024
f136795
ci fixes
vaind May 2, 2024
2aa129c
fix tests
vaind May 2, 2024
698276d
add jnigen scripts
vaind May 2, 2024
c6c3a17
use android 7.9.0 alpha.1
vaind May 2, 2024
cf8ed52
move native init & close to SentryNative
vaind May 2, 2024
55c7056
cleanup
vaind May 2, 2024
ff2a8ed
add macOS integration link
vaind May 6, 2024
46a96b0
Merge branch 'main' into feat/replay
vaind May 6, 2024
c3b60aa
rollback cocoa changes
vaind May 6, 2024
3f6d05e
remove jni/jnigen
vaind May 6, 2024
af22a59
wip: methodchannel based android recorder
vaind May 6, 2024
6bf1f00
callback
vaind May 6, 2024
9dc14aa
linter issues
vaind May 6, 2024
9daf297
Merge branch 'main' into feat/android-replay
vaind May 6, 2024
3639f00
minor fixes
vaind May 6, 2024
14ba742
more fixes
vaind May 6, 2024
585386e
linter issues
vaind May 6, 2024
ee1dbd6
cleanup
vaind May 6, 2024
266a85a
improve logging
vaind May 6, 2024
509c15f
move replay to experimental, same as in other SDKs
vaind May 7, 2024
960f2da
improve tree shaking
vaind May 7, 2024
b5c935f
Merge branch 'main' into feat/android-replay
vaind May 7, 2024
95e3a34
test: scheduler
vaind May 7, 2024
aa28200
support browser test
vaind May 7, 2024
16f3677
fix compat with old flutter
vaind May 7, 2024
86db5c4
cleanup
vaind May 7, 2024
942044a
rename recorder_widget_filter.dart
vaind May 7, 2024
9efae7b
fixup scheduler test
vaind May 7, 2024
35ed86b
improve test coverage
vaind May 7, 2024
63af017
pr cleanup
vaind May 7, 2024
12f5774
test: widget filter
vaind May 8, 2024
50a13f6
cleanup
vaind May 8, 2024
725fd02
test widget filter visibility
vaind May 8, 2024
4bda0ab
cleanup
vaind May 8, 2024
afb65f6
always add screenshot widget
vaind May 8, 2024
f6b9266
recorder test
vaind May 8, 2024
5dc1255
cleanup
vaind May 8, 2024
225c0c0
limit recorder test to vm
vaind May 8, 2024
46527a3
wip: integration test
vaind May 9, 2024
0bc8fff
cleanup
vaind May 9, 2024
571dfbc
Merge branch 'main' into feat/android-replay
vaind May 12, 2024
81f4689
ktlint format
vaind May 12, 2024
0f6764b
detekt suppression
vaind May 12, 2024
d35f630
ktlint format
vaind May 12, 2024
fee9580
improve scheduler stop behavior
vaind May 12, 2024
8be8d20
wip: error replay mapping
vaind May 13, 2024
c7b166d
Merge branch 'main' into feat/android-replay
vaind May 13, 2024
f3057cd
suppress detekt TooGenericExceptionThrown
vaind May 13, 2024
943acea
Update flutter/lib/src/replay/recorder.dart
vaind May 14, 2024
0d82f13
Update flutter/lib/src/native/java/sentry_native_java.dart
vaind May 14, 2024
49d4239
improve comments
vaind May 14, 2024
8ef5d15
Merge branch 'main' into feat/android-replay
vaind May 20, 2024
a93da0b
Merge branch 'main' into feat/android-replay
vaind May 28, 2024
114ed86
feat: associate dart errors with replays (#2070)
vaind Jun 13, 2024
5014d1c
Merge branch 'main' into feat/android-replay
vaind Jun 25, 2024
5baab7c
Merge branch 'main' into feat/android-replay
vaind Jun 27, 2024
c26a8a2
chote: remove path dependency
vaind Jun 27, 2024
e4c0654
Merge branch 'main' into feat/android-replay
vaind Jul 10, 2024
8919fff
Merge branch 'main' into feat/android-replay
vaind Jul 12, 2024
3f12988
fix tests
vaind Jul 12, 2024
1706c68
Merge branch 'main' into feat/android-replay
vaind Jul 12, 2024
5dc8bd6
feat: replay breadcrumbs (android) (#2163)
vaind Jul 17, 2024
26d7b9c
Merge branch 'main' into feat/android-replay
vaind Jul 17, 2024
8cd2d63
test: native replay integration binding (#2189)
vaind Jul 24, 2024
4883e14
Merge branch 'main' into feat/android-replay
vaind Jul 24, 2024
8bcde3d
Merge branch 'main' into feat/android-replay
vaind Jul 24, 2024
f1157fc
chore: update changelog
vaind Jul 24, 2024
93293f7
fix publishing
vaind Jul 24, 2024
87971db
release: 8.6.0-alpha.2
getsentry-bot Jul 24, 2024
3bf3cca
Merge branch 'release/8.6.0-alpha.2' into feat/android-replay
Jul 24, 2024
14340f7
Merge branch 'feat/replay' into feat/android-replay
vaind Aug 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions flutter/android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
buildscript {
ext.kotlin_version = '1.6.21'
ext.kotlin_version = '1.8.0'
repositories {
google()
mavenCentral()
}

dependencies {
classpath 'com.android.tools.build:gradle:7.2.2'
classpath 'com.android.tools.build:gradle:7.4.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
Expand Down Expand Up @@ -60,7 +60,7 @@ android {
}

dependencies {
api 'io.sentry:sentry-android:7.9.0'
api 'io.sentry:sentry-android:7.9.0-alpha.1'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"

// Required -- JUnit 4 framework
Expand Down
1 change: 1 addition & 0 deletions flutter/android/gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
1 change: 1 addition & 0 deletions flutter/android/gradlew
1 change: 1 addition & 0 deletions flutter/android/gradlew.bat
13 changes: 13 additions & 0 deletions flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.sentry.flutter

import io.sentry.SentryLevel
import io.sentry.SentryReplayOptions
import io.sentry.android.core.BuildConfig
import io.sentry.android.core.SentryAndroidOptions
import io.sentry.protocol.SdkVersion
Expand Down Expand Up @@ -119,6 +120,18 @@ class SentryFlutter(
data.getIfNotNull<Int>("readTimeoutMillis") {
options.readTimeoutMillis = it
}

data.getIfNotNull<Map<String, Any>>("replay") {
updateReplayOptions(options.experimental.sessionReplay, it)
}
}

fun updateReplayOptions(
options: SentryReplayOptions,
data: Map<String, Any>,
) {
options.sessionSampleRate = data["sessionSampleRate"] as? Double
options.errorSampleRate = data["errorSampleRate"] as? Double
}
}

Expand Down
147 changes: 124 additions & 23 deletions flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,20 @@ import io.sentry.android.core.SentryAndroid
import io.sentry.android.core.SentryAndroidOptions
import io.sentry.android.core.performance.AppStartMetrics
import io.sentry.android.core.performance.TimeSpan
import io.sentry.android.replay.ReplayIntegration
import io.sentry.protocol.DebugImage
import io.sentry.protocol.SdkVersion
import io.sentry.protocol.SentryId
import io.sentry.protocol.User
import io.sentry.transport.CurrentDateProvider
import java.io.File
import java.lang.ref.WeakReference

class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
private lateinit var channel: MethodChannel
private lateinit var context: Context
private lateinit var sentryFlutter: SentryFlutter
private lateinit var replay: ReplayIntegration

private var activity: WeakReference<Activity>? = null
private var framesTracker: ActivityFramesTracker? = null
Expand All @@ -53,7 +57,11 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
)
}

override fun onMethodCall(call: MethodCall, result: Result) {
@Suppress("CyclomaticComplexMethod")
override fun onMethodCall(
call: MethodCall,
result: Result,
) {
Comment on lines +62 to +65
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to use ktlint --format because of the boatload of complains by reviewdog 🤷

when (call.method) {
"initNativeSdk" -> initNativeSdk(call, result)
"captureEnvelope" -> captureEnvelope(call, result)
Expand All @@ -72,6 +80,8 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
"setTag" -> setTag(call.argument("key"), call.argument("value"), result)
"removeTag" -> removeTag(call.argument("key"), result)
"loadContexts" -> loadContexts(result)
"addReplayScreenshot" -> addReplayScreenshot(call.argument("path"), call.argument("timestamp"), result)
"sendReplayForEvent" -> sendReplayForEvent(call.argument("eventId"), call.argument("isCrash"), result)
else -> result.notImplemented()
}
}
Expand Down Expand Up @@ -101,7 +111,10 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
// Stub
}

private fun initNativeSdk(call: MethodCall, result: Result) {
private fun initNativeSdk(
call: MethodCall,
result: Result,
) {
if (!this::context.isInitialized) {
result.error("1", "Context is null", null)
return
Expand All @@ -121,6 +134,27 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
}

options.beforeSend = BeforeSendCallbackImpl(options.sdkVersion)

// Replace the default ReplayIntegration with a Flutter-specific recorder.
options.integrations.removeAll { it is ReplayIntegration }
val cacheDirPath = options.cacheDirPath
val replayOptions = options.experimental.sessionReplay
val isReplayEnabled = replayOptions.isSessionReplayEnabled || replayOptions.isSessionReplayForErrorsEnabled
if (cacheDirPath != null && isReplayEnabled) {
replay =
ReplayIntegration(
context,
dateProvider = CurrentDateProvider.getInstance(),
recorderProvider = { SentryFlutterReplayRecorder(channel, replay) },
recorderConfigProvider = null,
replayCacheProvider = null,
)

options.addIntegration(replay)
options.setReplayController(replay)
} else {
options.setReplayController(null)
}
}
result.success("")
}
Expand All @@ -143,6 +177,7 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
} else {
val appStartTimeMillis = DateUtils.nanosToMillis(appStartTime.nanoTimestamp().toDouble())
val item =

mutableMapOf<String, Any?>(
"pluginRegistrationTime" to pluginRegistrationTime,
"appStartTime" to appStartTimeMillis,
Expand Down Expand Up @@ -203,7 +238,10 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
result.success(null)
}

private fun endNativeFrames(id: String?, result: Result) {
private fun endNativeFrames(
id: String?,
result: Result,
) {
val activity = activity?.get()
if (!sentryFlutter.autoPerformanceTracingEnabled || activity == null || id == null) {
if (id == null) {
Expand All @@ -223,16 +261,21 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
if (total == 0 && slow == 0 && frozen == 0) {
result.success(null)
} else {
val frames = mapOf<String, Any?>(
"totalFrames" to total,
"slowFrames" to slow,
"frozenFrames" to frozen,
)
val frames =
mapOf<String, Any?>(
"totalFrames" to total,
"slowFrames" to slow,
"frozenFrames" to frozen,
)
result.success(frames)
}
}

private fun setContexts(key: String?, value: Any?, result: Result) {
private fun setContexts(
key: String?,
value: Any?,
result: Result,
) {
if (key == null || value == null) {
result.success("")
return
Expand All @@ -244,7 +287,10 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
}
}

private fun removeContexts(key: String?, result: Result) {
private fun removeContexts(
key: String?,
result: Result,
) {
if (key == null) {
result.success("")
return
Expand All @@ -256,7 +302,10 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
}
}

private fun setUser(user: Map<String, Any?>?, result: Result) {
private fun setUser(
user: Map<String, Any?>?,
result: Result,
) {
if (user != null) {
val options = HubAdapter.getInstance().options
val userInstance = User.fromMap(user, options)
Expand All @@ -267,7 +316,10 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
result.success("")
}

private fun addBreadcrumb(breadcrumb: Map<String, Any?>?, result: Result) {
private fun addBreadcrumb(
breadcrumb: Map<String, Any?>?,
result: Result,
) {
if (breadcrumb != null) {
val options = HubAdapter.getInstance().options
val breadcrumbInstance = Breadcrumb.fromMap(breadcrumb, options)
Expand All @@ -282,7 +334,11 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
result.success("")
}

private fun setExtra(key: String?, value: String?, result: Result) {
private fun setExtra(
key: String?,
value: String?,
result: Result,
) {
if (key == null || value == null) {
result.success("")
return
Expand All @@ -292,7 +348,10 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
result.success("")
}

private fun removeExtra(key: String?, result: Result) {
private fun removeExtra(
key: String?,
result: Result,
) {
if (key == null) {
result.success("")
return
Expand All @@ -302,7 +361,11 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
result.success("")
}

private fun setTag(key: String?, value: String?, result: Result) {
private fun setTag(
key: String?,
value: String?,
result: Result,
) {
if (key == null || value == null) {
result.success("")
return
Expand All @@ -312,7 +375,10 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
result.success("")
}

private fun removeTag(key: String?, result: Result) {
private fun removeTag(
key: String?,
result: Result,
) {
if (key == null) {
result.success("")
return
Expand All @@ -322,16 +388,19 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
result.success("")
}

private fun captureEnvelope(call: MethodCall, result: Result) {
private fun captureEnvelope(
call: MethodCall,
result: Result,
) {
if (!Sentry.isEnabled()) {
result.error("1", "The Sentry Android SDK is disabled", null)
return
}
val args = call.arguments() as List<Any>? ?: listOf()
if (args.isNotEmpty()) {
val event = args.first() as ByteArray?
if (event != null && event.isNotEmpty()) {
val id = InternalSentrySdk.captureEnvelope(event)
val envelopeData = args.first() as ByteArray?
if (envelopeData != null && envelopeData.isNotEmpty()) {
val id = InternalSentrySdk.captureEnvelope(envelopeData)
if (id != null) {
result.success("")
} else {
Expand Down Expand Up @@ -379,18 +448,21 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
private class BeforeSendCallbackImpl(
private val sdkVersion: SdkVersion?,
) : SentryOptions.BeforeSendCallback {
override fun execute(event: SentryEvent, hint: Hint): SentryEvent {
override fun execute(
event: SentryEvent,
hint: Hint,
): SentryEvent {
setEventOriginTag(event)
addPackages(event, sdkVersion)
return event
}
}

companion object {

private const val flutterSdk = "sentry.dart.flutter"
private const val androidSdk = "sentry.java.android.flutter"
private const val nativeSdk = "sentry.native.android.flutter"

private fun setEventOriginTag(event: SentryEvent) {
event.sdk?.let {
when (it.name) {
Expand All @@ -411,7 +483,10 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
event.setTag("event.environment", environment)
}

private fun addPackages(event: SentryEvent, sdk: SdkVersion?) {
private fun addPackages(
event: SentryEvent,
sdk: SdkVersion?,
) {
event.sdk?.let {
if (it.name == flutterSdk) {
sdk?.packageSet?.forEach { sentryPackage ->
Expand Down Expand Up @@ -440,4 +515,30 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
)
result.success(serializedScope)
}

private fun addReplayScreenshot(
path: String?,
timestamp: Long?,
result: Result,
) {
if (path == null || timestamp == null) {
result.error("5", "Arguments are null", null)
return
}
replay.onScreenshotRecorded(File(path), timestamp)
result.success("")
}

vaind marked this conversation as resolved.
Show resolved Hide resolved
private fun sendReplayForEvent(
eventId: String?,
isCrash: Boolean?,
result: Result,
) {
if (eventId == null || isCrash == null) {
result.error("5", "Arguments are null", null)
return
}
replay.sendReplay(isCrash, eventId, null)
result.success(replay.getReplayId().toString())
}
}
Loading
Loading