Skip to content

Commit f934ddf

Browse files
authored
Add sentry.replay_id to flutter logs (#3257)
1 parent 2b5e090 commit f934ddf

File tree

16 files changed

+477
-16
lines changed

16 files changed

+477
-16
lines changed

CHANGELOG.md

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

1212
- Refactor `AndroidReplayRecorder` to use the new worker isolate api [#3296](https://github.com/getsentry/sentry-dart/pull/3296/)
1313
- Offload `captureEnvelope` to background isolate for Cocoa and Android [#3232](https://github.com/getsentry/sentry-dart/pull/3232)
14+
- Add `sentry.replay_id` to flutter logs ([#3257](https://github.com/getsentry/sentry-dart/pull/3257))
1415

1516
## 9.7.0
1617

packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayRecorder.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import android.os.Handler
44
import android.os.Looper
55
import android.util.Log
66
import io.flutter.plugin.common.MethodChannel
7+
import io.sentry.Sentry
8+
import io.sentry.protocol.SentryId
79
import io.sentry.android.replay.Recorder
810
import io.sentry.android.replay.ReplayIntegration
911
import io.sentry.android.replay.ScreenshotRecorderConfig
@@ -15,10 +17,17 @@ internal class SentryFlutterReplayRecorder(
1517
override fun start() {
1618
Handler(Looper.getMainLooper()).post {
1719
try {
20+
val replayId = integration.getReplayId().toString()
21+
var replayIsBuffering = false
22+
Sentry.configureScope { scope ->
23+
// Buffering mode: we have a replay ID but it's not set on scope yet
24+
replayIsBuffering = scope.replayId == SentryId.EMPTY_ID
25+
}
1826
channel.invokeMethod(
1927
"ReplayRecorder.start",
2028
mapOf(
21-
"replayId" to integration.getReplayId().toString(),
29+
"replayId" to replayId,
30+
"replayIsBuffering" to replayIsBuffering,
2231
),
2332
)
2433
} catch (ignored: Exception) {

packages/flutter/example/lib/main.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ Future<void> setupSentry(
8787
options.maxRequestBodySize = MaxRequestBodySize.always;
8888
options.navigatorKey = navigatorKey;
8989

90-
options.replay.sessionSampleRate = 1.0;
90+
options.replay.sessionSampleRate = 0.0;
9191
options.replay.onErrorSampleRate = 1.0;
9292

9393
options.enableLogs = true;

packages/flutter/ios/sentry_flutter/Sources/sentry_flutter_objc/SentryFlutterReplayScreenshotProvider.m

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,15 @@ - (void)imageWithView:(UIView *_Nonnull)view
2525
// Replay ID may be null if session replay is disabled.
2626
// Replay is still captured for on-error replays.
2727
NSString *replayId = [PrivateSentrySDKOnly getReplayId];
28+
// On iOS, we only have access to scope's replay ID, so we cannot detect buffer mode
29+
// If replay ID exists, it's always in active session mode (not buffering)
30+
BOOL replayIsBuffering = NO;
2831
[self->channel
2932
invokeMethod:@"captureReplayScreenshot"
30-
arguments:@{@"replayId" : replayId ? replayId : [NSNull null]}
33+
arguments:@{
34+
@"replayId" : replayId ? replayId : [NSNull null],
35+
@"replayIsBuffering" : @(replayIsBuffering)
36+
}
3137
result:^(id value) {
3238
if (value == nil) {
3339
NSLog(@"SentryFlutterReplayScreenshotProvider received null "
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// ignore_for_file: invalid_use_of_internal_member
2+
3+
import 'package:sentry/sentry.dart';
4+
import '../sentry_flutter_options.dart';
5+
import '../native/sentry_native_binding.dart';
6+
7+
/// Integration that adds replay-related information to logs using lifecycle callbacks
8+
class ReplayLogIntegration implements Integration<SentryFlutterOptions> {
9+
static const String integrationName = 'ReplayLog';
10+
11+
final SentryNativeBinding? _native;
12+
ReplayLogIntegration(this._native);
13+
14+
SentryFlutterOptions? _options;
15+
SdkLifecycleCallback<OnBeforeCaptureLog>? _addReplayInformation;
16+
17+
@override
18+
Future<void> call(Hub hub, SentryFlutterOptions options) async {
19+
if (!options.replay.isEnabled) {
20+
return;
21+
}
22+
final sessionSampleRate = options.replay.sessionSampleRate ?? 0;
23+
final onErrorSampleRate = options.replay.onErrorSampleRate ?? 0;
24+
25+
_options = options;
26+
_addReplayInformation = (OnBeforeCaptureLog event) {
27+
final scopeReplayId = hub.scope.replayId;
28+
final replayId = scopeReplayId ?? _native?.replayId;
29+
final replayIsBuffering = replayId != null && scopeReplayId == null;
30+
31+
if (sessionSampleRate > 0 && replayId != null && !replayIsBuffering) {
32+
event.log.attributes['sentry.replay_id'] = SentryLogAttribute.string(
33+
scopeReplayId.toString(),
34+
);
35+
} else if (onErrorSampleRate > 0 &&
36+
replayId != null &&
37+
replayIsBuffering) {
38+
event.log.attributes['sentry.replay_id'] = SentryLogAttribute.string(
39+
replayId.toString(),
40+
);
41+
event.log.attributes['sentry._internal.replay_is_buffering'] =
42+
SentryLogAttribute.bool(true);
43+
}
44+
};
45+
options.lifecycleRegistry
46+
.registerCallback<OnBeforeCaptureLog>(_addReplayInformation!);
47+
options.sdk.addIntegration(integrationName);
48+
}
49+
50+
@override
51+
Future<void> close() async {
52+
final options = _options;
53+
final addReplayInformation = _addReplayInformation;
54+
55+
if (options != null && addReplayInformation != null) {
56+
options.lifecycleRegistry
57+
.removeCallback<OnBeforeCaptureLog>(addReplayInformation);
58+
}
59+
60+
_options = null;
61+
_addReplayInformation = null;
62+
}
63+
}

packages/flutter/lib/src/native/c/sentry_native.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,9 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding {
276276
@override
277277
bool get supportsReplay => false;
278278

279+
@override
280+
SentryId? get replayId => null;
281+
279282
@override
280283
FutureOr<void> setReplayConfig(ReplayConfig config) {
281284
_logNotSupported('replay config');

packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ class SentryNativeCocoa extends SentryNativeChannel {
2222
@override
2323
bool get supportsReplay => options.platform.isIOS;
2424

25+
@override
26+
SentryId? get replayId => _replayId;
27+
2528
@override
2629
Future<void> init(Hub hub) async {
2730
// We only need these when replay is enabled (session or error capture)
@@ -32,15 +35,20 @@ class SentryNativeCocoa extends SentryNativeChannel {
3235
case 'captureReplayScreenshot':
3336
_replayRecorder ??= CocoaReplayRecorder(options);
3437

35-
final replayId = call.arguments['replayId'] == null
38+
final replayIdArg = call.arguments['replayId'];
39+
final replayIsBuffering =
40+
call.arguments['replayIsBuffering'] as bool? ?? false;
41+
42+
final replayId = replayIdArg == null
3643
? null
37-
: SentryId.fromId(call.arguments['replayId'] as String);
44+
: SentryId.fromId(replayIdArg as String);
3845

3946
if (_replayId != replayId) {
4047
_replayId = replayId;
4148
hub.configureScope((s) {
49+
// Only set replay ID on scope if not buffering (active session mode)
4250
// ignore: invalid_use_of_internal_member
43-
s.replayId = replayId;
51+
s.replayId = !replayIsBuffering ? replayId : null;
4452
});
4553
}
4654

@@ -57,6 +65,13 @@ class SentryNativeCocoa extends SentryNativeChannel {
5765
return super.init(hub);
5866
}
5967

68+
@override
69+
FutureOr<SentryId> captureReplay() async {
70+
final replayId = await super.captureReplay();
71+
_replayId = replayId;
72+
return replayId;
73+
}
74+
6075
@override
6176
Future<void> close() async {
6277
await _envelopeSender?.close();

packages/flutter/lib/src/native/java/sentry_native_java.dart

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ class SentryNativeJava extends SentryNativeChannel {
2222
@override
2323
bool get supportsReplay => true;
2424

25+
@override
26+
SentryId? get replayId => _replayId;
27+
SentryId? _replayId;
28+
2529
@override
2630
Future<void> init(Hub hub) async {
2731
// We only need these when replay is enabled (session or error capture)
@@ -30,14 +34,25 @@ class SentryNativeJava extends SentryNativeChannel {
3034
channel.setMethodCallHandler((call) async {
3135
switch (call.method) {
3236
case 'ReplayRecorder.start':
33-
final replayId =
34-
SentryId.fromId(call.arguments['replayId'] as String);
37+
final replayIdArg = call.arguments['replayId'];
38+
final replayIsBufferingArg = call.arguments['replayIsBuffering'];
39+
40+
final replayId = replayIdArg != null
41+
? SentryId.fromId(replayIdArg as String)
42+
: null;
43+
44+
final replayIsBuffering = replayIsBufferingArg != null
45+
? replayIsBufferingArg as bool
46+
: false;
47+
48+
_replayId = replayId;
3549

3650
_replayRecorder = AndroidReplayRecorder.factory(options);
3751
await _replayRecorder!.start();
3852
hub.configureScope((s) {
53+
// Only set replay ID on scope if not buffering (active session mode)
3954
// ignore: invalid_use_of_internal_member
40-
s.replayId = replayId;
55+
s.replayId = !replayIsBuffering ? replayId : null;
4156
});
4257
break;
4358
case 'ReplayRecorder.onConfigurationChanged':
@@ -80,6 +95,13 @@ class SentryNativeJava extends SentryNativeChannel {
8095
return super.init(hub);
8196
}
8297

98+
@override
99+
FutureOr<SentryId> captureReplay() async {
100+
final replayId = await super.captureReplay();
101+
_replayId = replayId;
102+
return replayId;
103+
}
104+
83105
@override
84106
FutureOr<void> captureEnvelope(
85107
Uint8List envelopeData, bool containsUnhandledException) {

packages/flutter/lib/src/native/sentry_native_binding.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ abstract class SentryNativeBinding {
6464

6565
bool get supportsReplay;
6666

67+
SentryId? get replayId;
68+
6769
FutureOr<void> setReplayConfig(ReplayConfig config);
6870

6971
FutureOr<SentryId> captureReplay();

packages/flutter/lib/src/native/sentry_native_channel.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,9 @@ class SentryNativeChannel
240240
@override
241241
bool get supportsReplay => false;
242242

243+
@override
244+
SentryId? get replayId => null;
245+
243246
@override
244247
FutureOr<void> setReplayConfig(ReplayConfig config) =>
245248
channel.invokeMethod('setReplayConfig', {
@@ -251,7 +254,7 @@ class SentryNativeChannel
251254
});
252255

253256
@override
254-
Future<SentryId> captureReplay() => channel
257+
FutureOr<SentryId> captureReplay() => channel
255258
.invokeMethod('captureReplay')
256259
.then((value) => SentryId.fromId(value as String));
257260

0 commit comments

Comments
 (0)