Skip to content

Commit 914a9ec

Browse files
authored
feat: replay tags (#2592)
* refactor: replay options as tags in android * pass masking rule tags from flutter * change how closures are described * fix tests * add flutter sdk info to replay * fix: android packages propagation * add tags to ios replay * cleanup * add sdk version * ktlint format * lints * chore: changelog
1 parent ed08c68 commit 914a9ec

File tree

11 files changed

+520
-304
lines changed

11 files changed

+520
-304
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
### Enhancements
1111

1212
- Print a warning if the rate limit was reached ([#2595](https://github.com/getsentry/sentry-dart/pull/2595))
13+
- Add replay masking config to tags and report SDKs versions ([#2592](https://github.com/getsentry/sentry-dart/pull/2592))
1314

1415
### Fixes
1516

flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt

Lines changed: 84 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
11
package io.sentry.flutter
22

33
import android.util.Log
4+
import io.sentry.Hint
5+
import io.sentry.SentryEvent
46
import io.sentry.SentryLevel
7+
import io.sentry.SentryOptions
58
import io.sentry.SentryOptions.Proxy
69
import io.sentry.SentryReplayOptions
710
import io.sentry.android.core.BuildConfig
811
import io.sentry.android.core.SentryAndroidOptions
912
import io.sentry.protocol.SdkVersion
13+
import io.sentry.rrweb.RRWebOptionsEvent
1014
import java.net.Proxy.Type
1115
import java.util.Locale
1216

13-
class SentryFlutter(
14-
private val androidSdk: String,
15-
private val nativeSdk: String,
16-
) {
17+
class SentryFlutter {
18+
companion object {
19+
internal const val FLUTTER_SDK = "sentry.dart.flutter"
20+
internal const val ANDROID_SDK = "sentry.java.android.flutter"
21+
internal const val NATIVE_SDK = "sentry.native.android.flutter"
22+
}
23+
1724
var autoPerformanceTracingEnabled = false
1825

1926
fun updateOptions(
@@ -114,14 +121,29 @@ class SentryFlutter(
114121

115122
var sdkVersion = options.sdkVersion
116123
if (sdkVersion == null) {
117-
sdkVersion = SdkVersion(androidSdk, BuildConfig.VERSION_NAME)
124+
sdkVersion = SdkVersion(ANDROID_SDK, BuildConfig.VERSION_NAME)
118125
} else {
119-
sdkVersion.name = androidSdk
126+
sdkVersion.name = ANDROID_SDK
120127
}
121128

122129
options.sdkVersion = sdkVersion
123-
options.sentryClientName = "$androidSdk/${BuildConfig.VERSION_NAME}"
124-
options.nativeSdkName = nativeSdk
130+
options.sentryClientName = "$ANDROID_SDK/${BuildConfig.VERSION_NAME}"
131+
options.nativeSdkName = NATIVE_SDK
132+
133+
data.getIfNotNull<Map<String, Any>>("sdk") { flutterSdk ->
134+
flutterSdk.getIfNotNull<List<String>>("integrations") {
135+
it.forEach { integration ->
136+
sdkVersion.addIntegration(integration)
137+
}
138+
}
139+
flutterSdk.getIfNotNull<List<Map<String, String>>>("packages") {
140+
it.forEach { fPackage ->
141+
sdkVersion.addPackage(fPackage["name"] as String, fPackage["version"] as String)
142+
}
143+
}
144+
}
145+
146+
options.beforeSend = BeforeSendCallbackImpl()
125147

126148
data.getIfNotNull<Int>("connectionTimeoutMillis") {
127149
options.connectionTimeoutMillis = it
@@ -154,30 +176,51 @@ class SentryFlutter(
154176
}
155177
}
156178

157-
data.getIfNotNull<Map<String, Any>>("replay") {
158-
updateReplayOptions(options.sessionReplay, it)
179+
data.getIfNotNull<Map<String, Any>>("replay") { replayArgs ->
180+
updateReplayOptions(options, replayArgs)
181+
182+
data.getIfNotNull<Map<String, Any>>("sdk") {
183+
options.sessionReplay.sdkVersion = SdkVersion(it["name"] as String, it["version"] as String)
184+
}
159185
}
160186
}
161187

162-
fun updateReplayOptions(
163-
options: SentryReplayOptions,
188+
private fun updateReplayOptions(
189+
options: SentryAndroidOptions,
164190
data: Map<String, Any>,
165191
) {
166-
options.quality =
192+
val replayOptions = options.sessionReplay
193+
replayOptions.quality =
167194
when (data["quality"] as? String) {
168195
"low" -> SentryReplayOptions.SentryReplayQuality.LOW
169196
"high" -> SentryReplayOptions.SentryReplayQuality.HIGH
170197
else -> {
171198
SentryReplayOptions.SentryReplayQuality.MEDIUM
172199
}
173200
}
174-
options.sessionSampleRate = data["sessionSampleRate"] as? Double
175-
options.onErrorSampleRate = data["onErrorSampleRate"] as? Double
201+
replayOptions.sessionSampleRate = (data["sessionSampleRate"] as? Number)?.toDouble()
202+
replayOptions.onErrorSampleRate = (data["onErrorSampleRate"] as? Number)?.toDouble()
176203

177204
// Disable native tracking of orientation change (causes replay restart)
178205
// because we don't have the new size from Flutter yet. Instead, we'll
179206
// trigger onConfigurationChanged() manually in setReplayConfig().
180-
options.setTrackOrientationChange(false)
207+
replayOptions.setTrackOrientationChange(false)
208+
209+
@Suppress("UNCHECKED_CAST")
210+
val tags = (data["tags"] as? Map<String, Any>) ?: mapOf()
211+
options.beforeSendReplay =
212+
SentryOptions.BeforeSendReplayCallback { event, hint ->
213+
hint.replayRecording?.payload?.firstOrNull { it is RRWebOptionsEvent }?.let { optionsEvent ->
214+
val payload = (optionsEvent as RRWebOptionsEvent).optionsPayload
215+
216+
// Remove defaults set by the native SDK.
217+
payload.filterKeys { it.contains("mask") }.forEach { (k, _) -> payload.remove(k) }
218+
219+
// Now, set the Flutter-specific values.
220+
payload.putAll(tags)
221+
}
222+
event
223+
}
181224
}
182225
}
183226

@@ -191,3 +234,28 @@ private fun <T> Map<String, Any>.getIfNotNull(
191234
callback(it)
192235
}
193236
}
237+
238+
private class BeforeSendCallbackImpl : SentryOptions.BeforeSendCallback {
239+
override fun execute(
240+
event: SentryEvent,
241+
hint: Hint,
242+
): SentryEvent {
243+
event.sdk?.let {
244+
when (it.name) {
245+
SentryFlutter.FLUTTER_SDK -> setEventEnvironmentTag(event, "flutter", "dart")
246+
SentryFlutter.ANDROID_SDK -> setEventEnvironmentTag(event, environment = "java")
247+
SentryFlutter.NATIVE_SDK -> setEventEnvironmentTag(event, environment = "native")
248+
}
249+
}
250+
return event
251+
}
252+
253+
private fun setEventEnvironmentTag(
254+
event: SentryEvent,
255+
origin: String = "android",
256+
environment: String,
257+
) {
258+
event.setTag("event.origin", origin)
259+
event.setTag("event.environment", environment)
260+
}
261+
}

flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt

Lines changed: 36 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,8 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler
2020
import io.flutter.plugin.common.MethodChannel.Result
2121
import io.sentry.Breadcrumb
2222
import io.sentry.DateUtils
23-
import io.sentry.Hint
2423
import io.sentry.HubAdapter
2524
import io.sentry.Sentry
26-
import io.sentry.SentryEvent
27-
import io.sentry.SentryOptions
2825
import io.sentry.android.core.ActivityFramesTracker
2926
import io.sentry.android.core.InternalSentrySdk
3027
import io.sentry.android.core.LoadClass
@@ -35,10 +32,8 @@ import io.sentry.android.core.performance.TimeSpan
3532
import io.sentry.android.replay.ReplayIntegration
3633
import io.sentry.android.replay.ScreenshotRecorderConfig
3734
import io.sentry.protocol.DebugImage
38-
import io.sentry.protocol.SdkVersion
3935
import io.sentry.protocol.SentryId
4036
import io.sentry.protocol.User
41-
import io.sentry.rrweb.RRWebOptionsEvent
4237
import io.sentry.transport.CurrentDateProvider
4338
import java.io.File
4439
import java.lang.ref.WeakReference
@@ -79,11 +74,7 @@ class SentryFlutterPlugin :
7974
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "sentry_flutter")
8075
channel.setMethodCallHandler(this)
8176

82-
sentryFlutter =
83-
SentryFlutter(
84-
androidSdk = ANDROID_SDK,
85-
nativeSdk = NATIVE_SDK,
86-
)
77+
sentryFlutter = SentryFlutter()
8778
}
8879

8980
@Suppress("CyclomaticComplexMethod")
@@ -165,57 +156,45 @@ class SentryFlutterPlugin :
165156
framesTracker = ActivityFramesTracker(LoadClass(), options)
166157
}
167158

168-
options.beforeSend = BeforeSendCallbackImpl(options.sdkVersion)
169-
170-
// Replace the default ReplayIntegration with a Flutter-specific recorder.
171-
options.integrations.removeAll { it is ReplayIntegration }
172-
val cacheDirPath = options.cacheDirPath
173-
val replayOptions = options.sessionReplay
174-
val isReplayEnabled = replayOptions.isSessionReplayEnabled || replayOptions.isSessionReplayForErrorsEnabled
175-
if (cacheDirPath != null && isReplayEnabled) {
176-
replay =
177-
ReplayIntegration(
178-
context,
179-
dateProvider = CurrentDateProvider.getInstance(),
180-
recorderProvider = { SentryFlutterReplayRecorder(channel, replay) },
181-
recorderConfigProvider = {
182-
Log.i(
183-
"Sentry",
184-
"Replay configuration requested. Returning: %dx%d at %d FPS, %d BPS".format(
185-
replayConfig.recordingWidth,
186-
replayConfig.recordingHeight,
187-
replayConfig.frameRate,
188-
replayConfig.bitRate,
189-
),
190-
)
191-
replayConfig
192-
},
193-
replayCacheProvider = null,
194-
)
195-
replay.breadcrumbConverter = SentryFlutterReplayBreadcrumbConverter()
196-
options.addIntegration(replay)
197-
options.setReplayController(replay)
198-
199-
options.beforeSendReplay =
200-
SentryOptions.BeforeSendReplayCallback { event, hint ->
201-
hint.replayRecording?.payload?.firstOrNull { it is RRWebOptionsEvent }?.let { optionsEvent ->
202-
val payload = (optionsEvent as RRWebOptionsEvent).optionsPayload
203-
204-
// Remove defaults set by the native SDK.
205-
payload.filterKeys { it.contains("mask") }.forEach { (k, _) -> payload.remove(k) }
206-
207-
// Now, set the Flutter-specific values.
208-
// TODO do this in a followup PR
209-
}
210-
event
211-
}
212-
} else {
213-
options.setReplayController(null)
214-
}
159+
setupReplay(options)
215160
}
216161
result.success("")
217162
}
218163

164+
private fun setupReplay(options: SentryAndroidOptions) {
165+
// Replace the default ReplayIntegration with a Flutter-specific recorder.
166+
options.integrations.removeAll { it is ReplayIntegration }
167+
val cacheDirPath = options.cacheDirPath
168+
val replayOptions = options.sessionReplay
169+
val isReplayEnabled = replayOptions.isSessionReplayEnabled || replayOptions.isSessionReplayForErrorsEnabled
170+
if (cacheDirPath != null && isReplayEnabled) {
171+
replay =
172+
ReplayIntegration(
173+
context,
174+
dateProvider = CurrentDateProvider.getInstance(),
175+
recorderProvider = { SentryFlutterReplayRecorder(channel, replay) },
176+
recorderConfigProvider = {
177+
Log.i(
178+
"Sentry",
179+
"Replay configuration requested. Returning: %dx%d at %d FPS, %d BPS".format(
180+
replayConfig.recordingWidth,
181+
replayConfig.recordingHeight,
182+
replayConfig.frameRate,
183+
replayConfig.bitRate,
184+
),
185+
)
186+
replayConfig
187+
},
188+
replayCacheProvider = null,
189+
)
190+
replay.breadcrumbConverter = SentryFlutterReplayBreadcrumbConverter()
191+
options.addIntegration(replay)
192+
options.setReplayController(replay)
193+
} else {
194+
options.setReplayController(null)
195+
}
196+
}
197+
219198
private fun fetchNativeAppStart(result: Result) {
220199
if (!sentryFlutter.autoPerformanceTracingEnabled) {
221200
result.success(null)
@@ -537,61 +516,9 @@ class SentryFlutterPlugin :
537516
result.success("")
538517
}
539518

540-
private class BeforeSendCallbackImpl(
541-
private val sdkVersion: SdkVersion?,
542-
) : SentryOptions.BeforeSendCallback {
543-
override fun execute(
544-
event: SentryEvent,
545-
hint: Hint,
546-
): SentryEvent {
547-
setEventOriginTag(event)
548-
addPackages(event, sdkVersion)
549-
return event
550-
}
551-
}
552-
553519
companion object {
554-
private const val FLUTTER_SDK = "sentry.dart.flutter"
555-
private const val ANDROID_SDK = "sentry.java.android.flutter"
556-
private const val NATIVE_SDK = "sentry.native.android.flutter"
557520
private const val NATIVE_CRASH_WAIT_TIME = 500L
558521

559-
private fun setEventOriginTag(event: SentryEvent) {
560-
event.sdk?.let {
561-
when (it.name) {
562-
FLUTTER_SDK -> setEventEnvironmentTag(event, "flutter", "dart")
563-
ANDROID_SDK -> setEventEnvironmentTag(event, environment = "java")
564-
NATIVE_SDK -> setEventEnvironmentTag(event, environment = "native")
565-
else -> return
566-
}
567-
}
568-
}
569-
570-
private fun setEventEnvironmentTag(
571-
event: SentryEvent,
572-
origin: String = "android",
573-
environment: String,
574-
) {
575-
event.setTag("event.origin", origin)
576-
event.setTag("event.environment", environment)
577-
}
578-
579-
private fun addPackages(
580-
event: SentryEvent,
581-
sdk: SdkVersion?,
582-
) {
583-
event.sdk?.let {
584-
if (it.name == FLUTTER_SDK) {
585-
sdk?.packageSet?.forEach { sentryPackage ->
586-
it.addPackage(sentryPackage.name, sentryPackage.version)
587-
}
588-
sdk?.integrationSet?.forEach { integration ->
589-
it.addIntegration(integration)
590-
}
591-
}
592-
}
593-
}
594-
595522
private fun crash() {
596523
val exception = RuntimeException("FlutterSentry Native Integration: Sample RuntimeException")
597524
val mainThread = Looper.getMainLooper().thread

0 commit comments

Comments
 (0)