diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a736deffd..5462a616ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Refactor `AndroidReplayRecorder` to use the new worker isolate api [#3296](https://github.com/getsentry/sentry-dart/pull/3296/) - Offload `captureEnvelope` to background isolate for Cocoa and Android [#3232](https://github.com/getsentry/sentry-dart/pull/3232) +- Add `sentry.replay_id` to flutter logs ([#3257](https://github.com/getsentry/sentry-dart/pull/3257)) ## 9.7.0 diff --git a/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayRecorder.kt b/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayRecorder.kt index a7897c002b..bdb0c8f1db 100644 --- a/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayRecorder.kt +++ b/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayRecorder.kt @@ -4,6 +4,8 @@ import android.os.Handler import android.os.Looper import android.util.Log import io.flutter.plugin.common.MethodChannel +import io.sentry.Sentry +import io.sentry.protocol.SentryId import io.sentry.android.replay.Recorder import io.sentry.android.replay.ReplayIntegration import io.sentry.android.replay.ScreenshotRecorderConfig @@ -15,10 +17,17 @@ internal class SentryFlutterReplayRecorder( override fun start() { Handler(Looper.getMainLooper()).post { try { + val replayId = integration.getReplayId().toString() + var replayIsBuffering = false + Sentry.configureScope { scope -> + // Buffering mode: we have a replay ID but it's not set on scope yet + replayIsBuffering = scope.replayId == SentryId.EMPTY_ID + } channel.invokeMethod( "ReplayRecorder.start", mapOf( - "replayId" to integration.getReplayId().toString(), + "replayId" to replayId, + "replayIsBuffering" to replayIsBuffering, ), ) } catch (ignored: Exception) { diff --git a/packages/flutter/example/lib/main.dart b/packages/flutter/example/lib/main.dart index 0f936268d9..ff7810c58b 100644 --- a/packages/flutter/example/lib/main.dart +++ b/packages/flutter/example/lib/main.dart @@ -87,7 +87,7 @@ Future setupSentry( options.maxRequestBodySize = MaxRequestBodySize.always; options.navigatorKey = navigatorKey; - options.replay.sessionSampleRate = 1.0; + options.replay.sessionSampleRate = 0.0; options.replay.onErrorSampleRate = 1.0; options.enableLogs = true; diff --git a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter_objc/SentryFlutterReplayScreenshotProvider.m b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter_objc/SentryFlutterReplayScreenshotProvider.m index d8910fd98b..e22f4f9df6 100644 --- a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter_objc/SentryFlutterReplayScreenshotProvider.m +++ b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter_objc/SentryFlutterReplayScreenshotProvider.m @@ -25,9 +25,15 @@ - (void)imageWithView:(UIView *_Nonnull)view // Replay ID may be null if session replay is disabled. // Replay is still captured for on-error replays. NSString *replayId = [PrivateSentrySDKOnly getReplayId]; + // On iOS, we only have access to scope's replay ID, so we cannot detect buffer mode + // If replay ID exists, it's always in active session mode (not buffering) + BOOL replayIsBuffering = NO; [self->channel invokeMethod:@"captureReplayScreenshot" - arguments:@{@"replayId" : replayId ? replayId : [NSNull null]} + arguments:@{ + @"replayId" : replayId ? replayId : [NSNull null], + @"replayIsBuffering" : @(replayIsBuffering) + } result:^(id value) { if (value == nil) { NSLog(@"SentryFlutterReplayScreenshotProvider received null " diff --git a/packages/flutter/lib/src/integrations/replay_log_integration.dart b/packages/flutter/lib/src/integrations/replay_log_integration.dart new file mode 100644 index 0000000000..99266a5328 --- /dev/null +++ b/packages/flutter/lib/src/integrations/replay_log_integration.dart @@ -0,0 +1,63 @@ +// ignore_for_file: invalid_use_of_internal_member + +import 'package:sentry/sentry.dart'; +import '../sentry_flutter_options.dart'; +import '../native/sentry_native_binding.dart'; + +/// Integration that adds replay-related information to logs using lifecycle callbacks +class ReplayLogIntegration implements Integration { + static const String integrationName = 'ReplayLog'; + + final SentryNativeBinding? _native; + ReplayLogIntegration(this._native); + + SentryFlutterOptions? _options; + SdkLifecycleCallback? _addReplayInformation; + + @override + Future call(Hub hub, SentryFlutterOptions options) async { + if (!options.replay.isEnabled) { + return; + } + final sessionSampleRate = options.replay.sessionSampleRate ?? 0; + final onErrorSampleRate = options.replay.onErrorSampleRate ?? 0; + + _options = options; + _addReplayInformation = (OnBeforeCaptureLog event) { + final scopeReplayId = hub.scope.replayId; + final replayId = scopeReplayId ?? _native?.replayId; + final replayIsBuffering = replayId != null && scopeReplayId == null; + + if (sessionSampleRate > 0 && replayId != null && !replayIsBuffering) { + event.log.attributes['sentry.replay_id'] = SentryLogAttribute.string( + scopeReplayId.toString(), + ); + } else if (onErrorSampleRate > 0 && + replayId != null && + replayIsBuffering) { + event.log.attributes['sentry.replay_id'] = SentryLogAttribute.string( + replayId.toString(), + ); + event.log.attributes['sentry._internal.replay_is_buffering'] = + SentryLogAttribute.bool(true); + } + }; + options.lifecycleRegistry + .registerCallback(_addReplayInformation!); + options.sdk.addIntegration(integrationName); + } + + @override + Future close() async { + final options = _options; + final addReplayInformation = _addReplayInformation; + + if (options != null && addReplayInformation != null) { + options.lifecycleRegistry + .removeCallback(addReplayInformation); + } + + _options = null; + _addReplayInformation = null; + } +} diff --git a/packages/flutter/lib/src/native/c/sentry_native.dart b/packages/flutter/lib/src/native/c/sentry_native.dart index 793d103a23..85c0b602c6 100644 --- a/packages/flutter/lib/src/native/c/sentry_native.dart +++ b/packages/flutter/lib/src/native/c/sentry_native.dart @@ -276,6 +276,9 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { @override bool get supportsReplay => false; + @override + SentryId? get replayId => null; + @override FutureOr setReplayConfig(ReplayConfig config) { _logNotSupported('replay config'); diff --git a/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart b/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart index 0beb6e26b1..ba5ceed0c5 100644 --- a/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart +++ b/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart @@ -22,6 +22,9 @@ class SentryNativeCocoa extends SentryNativeChannel { @override bool get supportsReplay => options.platform.isIOS; + @override + SentryId? get replayId => _replayId; + @override Future init(Hub hub) async { // We only need these when replay is enabled (session or error capture) @@ -32,15 +35,20 @@ class SentryNativeCocoa extends SentryNativeChannel { case 'captureReplayScreenshot': _replayRecorder ??= CocoaReplayRecorder(options); - final replayId = call.arguments['replayId'] == null + final replayIdArg = call.arguments['replayId']; + final replayIsBuffering = + call.arguments['replayIsBuffering'] as bool? ?? false; + + final replayId = replayIdArg == null ? null - : SentryId.fromId(call.arguments['replayId'] as String); + : SentryId.fromId(replayIdArg as String); if (_replayId != replayId) { _replayId = replayId; hub.configureScope((s) { + // Only set replay ID on scope if not buffering (active session mode) // ignore: invalid_use_of_internal_member - s.replayId = replayId; + s.replayId = !replayIsBuffering ? replayId : null; }); } @@ -57,6 +65,13 @@ class SentryNativeCocoa extends SentryNativeChannel { return super.init(hub); } + @override + FutureOr captureReplay() async { + final replayId = await super.captureReplay(); + _replayId = replayId; + return replayId; + } + @override Future close() async { await _envelopeSender?.close(); diff --git a/packages/flutter/lib/src/native/java/sentry_native_java.dart b/packages/flutter/lib/src/native/java/sentry_native_java.dart index 58fb8657ce..c7f14cc121 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -22,6 +22,10 @@ class SentryNativeJava extends SentryNativeChannel { @override bool get supportsReplay => true; + @override + SentryId? get replayId => _replayId; + SentryId? _replayId; + @override Future init(Hub hub) async { // We only need these when replay is enabled (session or error capture) @@ -30,14 +34,25 @@ class SentryNativeJava extends SentryNativeChannel { channel.setMethodCallHandler((call) async { switch (call.method) { case 'ReplayRecorder.start': - final replayId = - SentryId.fromId(call.arguments['replayId'] as String); + final replayIdArg = call.arguments['replayId']; + final replayIsBufferingArg = call.arguments['replayIsBuffering']; + + final replayId = replayIdArg != null + ? SentryId.fromId(replayIdArg as String) + : null; + + final replayIsBuffering = replayIsBufferingArg != null + ? replayIsBufferingArg as bool + : false; + + _replayId = replayId; _replayRecorder = AndroidReplayRecorder.factory(options); await _replayRecorder!.start(); hub.configureScope((s) { + // Only set replay ID on scope if not buffering (active session mode) // ignore: invalid_use_of_internal_member - s.replayId = replayId; + s.replayId = !replayIsBuffering ? replayId : null; }); break; case 'ReplayRecorder.onConfigurationChanged': @@ -80,6 +95,13 @@ class SentryNativeJava extends SentryNativeChannel { return super.init(hub); } + @override + FutureOr captureReplay() async { + final replayId = await super.captureReplay(); + _replayId = replayId; + return replayId; + } + @override FutureOr captureEnvelope( Uint8List envelopeData, bool containsUnhandledException) { diff --git a/packages/flutter/lib/src/native/sentry_native_binding.dart b/packages/flutter/lib/src/native/sentry_native_binding.dart index db9d17916c..bf45423d25 100644 --- a/packages/flutter/lib/src/native/sentry_native_binding.dart +++ b/packages/flutter/lib/src/native/sentry_native_binding.dart @@ -64,6 +64,8 @@ abstract class SentryNativeBinding { bool get supportsReplay; + SentryId? get replayId; + FutureOr setReplayConfig(ReplayConfig config); FutureOr captureReplay(); diff --git a/packages/flutter/lib/src/native/sentry_native_channel.dart b/packages/flutter/lib/src/native/sentry_native_channel.dart index 20d72dd317..10e96c08c9 100644 --- a/packages/flutter/lib/src/native/sentry_native_channel.dart +++ b/packages/flutter/lib/src/native/sentry_native_channel.dart @@ -240,6 +240,9 @@ class SentryNativeChannel @override bool get supportsReplay => false; + @override + SentryId? get replayId => null; + @override FutureOr setReplayConfig(ReplayConfig config) => channel.invokeMethod('setReplayConfig', { @@ -251,7 +254,7 @@ class SentryNativeChannel }); @override - Future captureReplay() => channel + FutureOr captureReplay() => channel .invokeMethod('captureReplay') .then((value) => SentryId.fromId(value as String)); diff --git a/packages/flutter/lib/src/sentry_flutter.dart b/packages/flutter/lib/src/sentry_flutter.dart index 03291edf1a..d110b43e41 100644 --- a/packages/flutter/lib/src/sentry_flutter.dart +++ b/packages/flutter/lib/src/sentry_flutter.dart @@ -22,6 +22,7 @@ import 'integrations/flutter_framework_feature_flag_integration.dart'; import 'integrations/frames_tracking_integration.dart'; import 'integrations/integrations.dart'; import 'integrations/native_app_start_handler.dart'; +import 'integrations/replay_log_integration.dart'; import 'integrations/screenshot_integration.dart'; import 'integrations/generic_app_start_integration.dart'; import 'integrations/thread_info_integration.dart'; @@ -230,6 +231,11 @@ mixin SentryFlutter { integrations.add(DebugPrintIntegration()); + // Only add ReplayLogIntegration on platforms that support replay + if (native != null && native.supportsReplay) { + integrations.add(ReplayLogIntegration(native)); + } + if (!platform.isWeb) { integrations.add(ThreadInfoIntegration()); } diff --git a/packages/flutter/lib/src/web/sentry_web.dart b/packages/flutter/lib/src/web/sentry_web.dart index 5a87bdc263..05fe22e09b 100644 --- a/packages/flutter/lib/src/web/sentry_web.dart +++ b/packages/flutter/lib/src/web/sentry_web.dart @@ -272,6 +272,9 @@ class SentryWeb with SentryNativeSafeInvoker implements SentryNativeBinding { @override bool get supportsReplay => false; + @override + SentryId? get replayId => null; + @override SentryFlutterOptions get options => _options; } diff --git a/packages/flutter/test/integrations/replay_log_integration_test.dart b/packages/flutter/test/integrations/replay_log_integration_test.dart new file mode 100644 index 0000000000..f03fada734 --- /dev/null +++ b/packages/flutter/test/integrations/replay_log_integration_test.dart @@ -0,0 +1,303 @@ +// ignore_for_file: invalid_use_of_internal_member + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry_flutter/src/integrations/replay_log_integration.dart'; + +import '../mocks.mocks.dart'; + +void main() { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + group('ReplayLogIntegration', () { + test('does not register when replay is disabled', () async { + final integration = fixture.getSut(); + fixture.options.replay.sessionSampleRate = 0.0; + fixture.options.replay.onErrorSampleRate = 0.0; + + await integration.call(fixture.hub, fixture.options); + + // Integration should not be registered when replay is disabled + expect(fixture.options.sdk.integrations.contains('ReplayLog'), false); + }); + + test( + 'adds replay_id attribute when sessionSampleRate > 0 and scope replayId is set', + () async { + final integration = fixture.getSut(); + + fixture.options.replay.sessionSampleRate = 0.5; + fixture.hub.scope.replayId = SentryId.fromId('test-replay-id'); + + await integration.call(fixture.hub, fixture.options); + + final log = fixture.createTestLog(); + await fixture.hub.captureLog(log); + + expect(log.attributes['sentry.replay_id']?.value, 'testreplayid'); + expect(log.attributes['sentry.replay_id']?.type, 'string'); + + // When scope replayId is set via session sample rate, no buffering flag should be added + expect(log.attributes.containsKey('sentry._internal.replay_is_buffering'), + false); + }); + + test( + 'does not add replay_id when sessionSampleRate is 0 even if scope replayId is set', + () async { + final integration = fixture.getSut(); + + fixture.options.replay.sessionSampleRate = 0.0; + fixture.options.replay.onErrorSampleRate = 0.5; // Needed to enable replay + fixture.hub.scope.replayId = SentryId.fromId('test-replay-id'); + + await integration.call(fixture.hub, fixture.options); + + final log = fixture.createTestLog(); + await fixture.hub.captureLog(log); + + // With sessionSampleRate = 0, scope replay ID should not be used + // (even though it's set, we're not in session mode) + expect(log.attributes.containsKey('sentry.replay_id'), false); + expect(log.attributes.containsKey('sentry._internal.replay_is_buffering'), + false); + }); + + test( + 'does not add replay_id when sessionSampleRate is null even if scope replayId is set', + () async { + final integration = fixture.getSut(); + + fixture.options.replay.sessionSampleRate = null; + fixture.options.replay.onErrorSampleRate = 0.5; // Needed to enable replay + fixture.hub.scope.replayId = SentryId.fromId('test-replay-id'); + + await integration.call(fixture.hub, fixture.options); + + final log = fixture.createTestLog(); + await fixture.hub.captureLog(log); + + // With sessionSampleRate = null (treated as 0), scope replay ID should not be used + expect(log.attributes.containsKey('sentry.replay_id'), false); + expect(log.attributes.containsKey('sentry._internal.replay_is_buffering'), + false); + }); + + test( + 'uses replay_id when set on scope and sessionSampleRate > 0 (active session mode)', + () async { + final integration = fixture.getSut(); + + fixture.options.replay.sessionSampleRate = 0.5; + fixture.options.replay.onErrorSampleRate = 0.5; + final replayId = SentryId.fromId('test-replay-id'); + fixture.hub.scope.replayId = replayId; + + // Mock native replayId with the same value (same replay, just also set on scope) + when(fixture.nativeBinding.replayId).thenReturn(replayId); + + await integration.call(fixture.hub, fixture.options); + + final log = fixture.createTestLog(); + await fixture.hub.captureLog(log); + + // Should use the replay ID from scope (active session mode) + expect(log.attributes['sentry.replay_id']?.value, 'testreplayid'); + expect(log.attributes['sentry.replay_id']?.type, 'string'); + + // Should NOT add buffering flag when replay is active on scope + expect(log.attributes.containsKey('sentry._internal.replay_is_buffering'), + false); + }); + + test( + 'adds replay_id and buffering flag when replay is in buffer mode (scope null, native has ID)', + () async { + final integration = fixture.getSut(); + fixture.options.replay.onErrorSampleRate = 0.5; + // Scope replay ID is null (default), so we're in buffer mode + + // Mock native replayId to simulate buffering mode (same replay, not on scope yet) + final replayId = SentryId.fromId('test-replay-id'); + when(fixture.nativeBinding.replayId).thenReturn(replayId); + + await integration.call(fixture.hub, fixture.options); + + final log = fixture.createTestLog(); + await fixture.hub.captureLog(log); + + // In buffering mode, use native replay ID and add buffering flag + expect(log.attributes['sentry.replay_id']?.value, 'testreplayid'); + expect(log.attributes['sentry.replay_id']?.type, 'string'); + + expect( + log.attributes['sentry._internal.replay_is_buffering']?.value, true); + expect(log.attributes['sentry._internal.replay_is_buffering']?.type, + 'boolean'); + }); + + test( + 'does not add anything when onErrorSampleRate is 0 and no scope replayId', + () async { + final integration = fixture.getSut(); + fixture.options.replay.sessionSampleRate = 0.5; // Needed to enable replay + fixture.options.replay.onErrorSampleRate = 0.0; + // Scope replay ID is null (default) + + // Mock native replayId to simulate what would be buffering mode + final replayId = SentryId.fromId('test-replay-id'); + when(fixture.nativeBinding.replayId).thenReturn(replayId); + + await integration.call(fixture.hub, fixture.options); + + final log = fixture.createTestLog(); + await fixture.hub.captureLog(log); + + // When onErrorSampleRate is 0, native replayId should be ignored even if it exists + expect(log.attributes.containsKey('sentry.replay_id'), false); + expect(log.attributes.containsKey('sentry._internal.replay_is_buffering'), + false); + }); + + test( + 'does not add anything when onErrorSampleRate is null and no scope replayId', + () async { + final integration = fixture.getSut(); + fixture.options.replay.sessionSampleRate = 0.5; // Needed to enable replay + fixture.options.replay.onErrorSampleRate = null; + // Scope replay ID is null (default) + + // Mock native replayId to simulate what would be buffering mode + final replayId = SentryId.fromId('test-replay-id'); + when(fixture.nativeBinding.replayId).thenReturn(replayId); + + await integration.call(fixture.hub, fixture.options); + + final log = fixture.createTestLog(); + await fixture.hub.captureLog(log); + + // When onErrorSampleRate is null (treated as 0), native replayId should be ignored + expect(log.attributes.containsKey('sentry.replay_id'), false); + expect(log.attributes.containsKey('sentry._internal.replay_is_buffering'), + false); + }); + + test( + 'adds replay_id when scope is null but native has ID and onErrorSampleRate > 0 (buffer mode)', + () async { + final integration = fixture.getSut(); + fixture.options.replay.sessionSampleRate = 0.0; + fixture.options.replay.onErrorSampleRate = 0.5; + // Scope replay ID is null (default), so we're in buffer mode + + // Mock native replayId to simulate buffering mode (replay exists but not on scope) + final replayId = SentryId.fromId('test-replay-id'); + when(fixture.nativeBinding.replayId).thenReturn(replayId); + + await integration.call(fixture.hub, fixture.options); + + final log = fixture.createTestLog(); + await fixture.hub.captureLog(log); + + // When scope is null but native has replay ID, use it in buffer mode + expect(log.attributes['sentry.replay_id']?.value, 'testreplayid'); + expect(log.attributes['sentry.replay_id']?.type, 'string'); + expect( + log.attributes['sentry._internal.replay_is_buffering']?.value, true); + expect(log.attributes['sentry._internal.replay_is_buffering']?.type, + 'boolean'); + }); + + test('registers integration name in SDK with sessionSampleRate', () async { + final integration = fixture.getSut(); + + fixture.options.replay.sessionSampleRate = 0.5; + fixture.hub.scope.replayId = SentryId.fromId('test-replay-id'); + + await integration.call(fixture.hub, fixture.options); + + // Integration name is registered in SDK + expect(fixture.options.sdk.integrations.contains('ReplayLog'), true); + }); + + test('registers integration name in SDK with onErrorSampleRate', () async { + final integration = fixture.getSut(); + + fixture.options.replay.onErrorSampleRate = 0.5; + + // Mock native replayId + final replayId = SentryId.fromId('test-replay-id'); + when(fixture.nativeBinding.replayId).thenReturn(replayId); + + await integration.call(fixture.hub, fixture.options); + + // Integration name is registered in SDK + expect(fixture.options.sdk.integrations.contains('ReplayLog'), true); + }); + + test('removes callback on close', () async { + final integration = fixture.getSut(); + + fixture.options.replay.sessionSampleRate = 0.5; + fixture.hub.scope.replayId = SentryId.fromId('test-replay-id'); + + await integration.call(fixture.hub, fixture.options); + await integration.close(); + + final log = fixture.createTestLog(); + await fixture.hub.captureLog(log); + + expect(log.attributes.containsKey('sentry.replay_id'), false); + expect(log.attributes.containsKey('sentry._internal.replay_is_buffering'), + false); + }); + + test('integration name is correct', () { + expect(ReplayLogIntegration.integrationName, 'ReplayLog'); + }); + }); +} + +class Fixture { + final options = + SentryFlutterOptions(dsn: 'https://abc@def.ingest.sentry.io/1234567'); + final hub = MockHub(); + final nativeBinding = MockSentryNativeBinding(); + + Fixture() { + options.enableLogs = true; + options.environment = 'test'; + options.release = 'test-release'; + + final scope = Scope(options); + when(hub.options).thenReturn(options); + when(hub.scope).thenReturn(scope); + when(hub.captureLog(any)).thenAnswer((invocation) async { + final log = invocation.positionalArguments.first as SentryLog; + // Trigger the lifecycle callback + await options.lifecycleRegistry.dispatchCallback(OnBeforeCaptureLog(log)); + }); + + // Default: no native replayId + when(nativeBinding.replayId).thenReturn(null); + } + + SentryLog createTestLog() { + return SentryLog( + timestamp: DateTime.now(), + traceId: SentryId.newId(), + level: SentryLogLevel.info, + body: 'test log message', + attributes: {}, + ); + } + + ReplayLogIntegration getSut() { + return ReplayLogIntegration(nativeBinding); + } +} diff --git a/packages/flutter/test/replay/replay_native_test.dart b/packages/flutter/test/replay/replay_native_test.dart index 0641f95685..978d467bcd 100644 --- a/packages/flutter/test/replay/replay_native_test.dart +++ b/packages/flutter/test/replay/replay_native_test.dart @@ -77,7 +77,12 @@ void main() { verifyNever(hub.configureScope(any)); when(hub.configureScope(captureAny)).thenReturn(null); - final replayConfig = {'replayId': '123'}; + // Both platforms now use 'replayId' and 'replayIsBuffering' + // replayIsBuffering: false means replay ID should be set on scope (active session) + final replayConfig = { + 'replayId': '123', + 'replayIsBuffering': false, + }; // emulate the native platform invoking the method final future = native.invokeFromNative( @@ -94,7 +99,7 @@ void main() { final scope = Scope(options); expect(scope.replayId, isNull); await closure(scope); - expect(scope.replayId.toString(), replayConfig['replayId']); + expect(scope.replayId.toString(), '123'); if (mockPlatform.isAndroid) { await native.invokeFromNative('ReplayRecorder.stop'); @@ -138,7 +143,10 @@ void main() { await tester.pumpAndWaitUntil(future, requiredToComplete: wait); } - final Map replayConfig = {'replayId': '123'}; + final Map replayConfig = { + 'scope.replayId': '123', + 'replayId': '456', + }; final configuration = { 'width': 800, 'height': 600, @@ -177,7 +185,9 @@ void main() { await nextFrame(wait: false); expect(mockAndroidRecorder.captured.length, equals(count)); } else if (mockPlatform.isIOS) { - final Map replayConfig = {'replayId': '123'}; + final Map replayConfig = { + 'scope.replayId': '123' + }; Future captureAndVerify() async { final future = native.invokeFromNative( @@ -196,7 +206,7 @@ void main() { // Check everything works if session-replay rate is 0, // which causes replayId to be 0 as well. - replayConfig['replayId'] = null; + replayConfig['scope.replayId'] = null; await captureAndVerify(); } else { fail('unsupported platform'); diff --git a/packages/flutter/test/sentry_flutter_test.dart b/packages/flutter/test/sentry_flutter_test.dart index 5da3e3d5cf..30810e0880 100644 --- a/packages/flutter/test/sentry_flutter_test.dart +++ b/packages/flutter/test/sentry_flutter_test.dart @@ -10,6 +10,7 @@ import 'package:sentry_flutter/src/file_system_transport.dart'; import 'package:sentry_flutter/src/flutter_exception_type_identifier.dart'; import 'package:sentry_flutter/src/integrations/connectivity/connectivity_integration.dart'; import 'package:sentry_flutter/src/integrations/integrations.dart'; +import 'package:sentry_flutter/src/integrations/replay_log_integration.dart'; import 'package:sentry_flutter/src/integrations/screenshot_integration.dart'; import 'package:sentry_flutter/src/integrations/generic_app_start_integration.dart'; import 'package:sentry_flutter/src/integrations/thread_info_integration.dart'; @@ -49,6 +50,11 @@ final nonWebIntegrations = [ ThreadInfoIntegration, ]; +// These platforms support replay functionality +final replaySupportedIntegrations = [ + ReplayLogIntegration, +]; + // These should be added to Android final androidIntegrations = [ LoadContextsIntegration, @@ -106,6 +112,7 @@ void main() { ...androidIntegrations, ...platformAgnosticIntegrations, ...nonWebIntegrations, + ...replaySupportedIntegrations, ReplayIntegration, ], shouldNotHaveIntegrations: [ @@ -164,6 +171,7 @@ void main() { ...iOsAndMacOsIntegrations, ...platformAgnosticIntegrations, ...nonWebIntegrations, + ...replaySupportedIntegrations, ReplayIntegration, ], shouldNotHaveIntegrations: [ @@ -220,6 +228,7 @@ void main() { ], shouldNotHaveIntegrations: [ ...androidIntegrations, ...nonWebIntegrations, + ...replaySupportedIntegrations, ]); testBefore( @@ -270,6 +279,7 @@ void main() { ...androidIntegrations, ...iOsAndMacOsIntegrations, ...webIntegrations, + ...replaySupportedIntegrations, ], ); @@ -319,6 +329,7 @@ void main() { ...androidIntegrations, ...iOsAndMacOsIntegrations, ...webIntegrations, + ...replaySupportedIntegrations, ], ); @@ -369,6 +380,7 @@ void main() { ...androidIntegrations, ...iOsAndMacOsIntegrations, ...nonWebIntegrations, + ...replaySupportedIntegrations, ], ); @@ -440,6 +452,7 @@ void main() { ...androidIntegrations, ...iOsAndMacOsIntegrations, ...nonWebIntegrations, + ...replaySupportedIntegrations, ], ); @@ -485,6 +498,7 @@ void main() { ...androidIntegrations, ...iOsAndMacOsIntegrations, ...nonWebIntegrations, + ...replaySupportedIntegrations, ], ); @@ -530,6 +544,7 @@ void main() { ...androidIntegrations, ...iOsAndMacOsIntegrations, ...nonWebIntegrations, + ...replaySupportedIntegrations, ], ); diff --git a/packages/flutter/test/sentry_flutter_util.dart b/packages/flutter/test/sentry_flutter_util.dart index f951c95c5e..203815fe02 100644 --- a/packages/flutter/test/sentry_flutter_util.dart +++ b/packages/flutter/test/sentry_flutter_util.dart @@ -36,7 +36,7 @@ void testConfiguration({ shouldNotHaveIntegrations = Set.of(shouldNotHaveIntegrations) .difference(Set.of(shouldHaveIntegrations)); for (final type in shouldNotHaveIntegrations) { - expect(integrations, isNot(contains(type))); + expect(integrations.any((i) => i.runtimeType == type), false); } Integration? nativeIntegration;