diff --git a/packages/flutter/lib/src/integrations/replay_telemetry_integration.dart b/packages/flutter/lib/src/integrations/replay_telemetry_integration.dart index 9c8b229364..681b4d757a 100644 --- a/packages/flutter/lib/src/integrations/replay_telemetry_integration.dart +++ b/packages/flutter/lib/src/integrations/replay_telemetry_integration.dart @@ -61,6 +61,11 @@ class ReplayTelemetryIntegration implements Integration { if (attributes != null) { event.span.setAttributes(attributes); } + + final span = event.span; + if (identical(span, span.segmentSpan)) { + return _native?.registerSegmentName(span.name); + } }; _onGenerateNewTrace = (OnGenerateNewTrace event) async { diff --git a/packages/flutter/lib/src/native/c/sentry_native.dart b/packages/flutter/lib/src/native/c/sentry_native.dart index ed3390e4a4..dc4a7d8422 100644 --- a/packages/flutter/lib/src/native/c/sentry_native.dart +++ b/packages/flutter/lib/src/native/c/sentry_native.dart @@ -329,6 +329,11 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { FutureOr registerTraceId(SentryId traceId) { // No-op. Replay trace ID registration is currently Android-only. } + + @override + FutureOr registerSegmentName(String segmentName) { + // No-op. Replay segment name registration is currently Android-only. + } } extension SentryValueExtension on binding.sentry_value_u { 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 6e65192312..ad3aebdb38 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -294,6 +294,22 @@ class SentryNativeJava extends SentryNativeChannel { }); }); } + + @override + void registerSegmentName(String segmentName) { + if (segmentName.isEmpty) { + return; + } + + tryCatchSync('registerSegmentName', () { + using((arena) { + final jSegmentName = segmentName.toJString()..releasedBy(arena); + _nativeReplay ??= + native.SentryFlutterPlugin.privateSentryGetReplayIntegration(); + _nativeReplay?.registerSegmentName(jSegmentName); + }); + }); + } } // Direct JNI conversion is fine for primitives. Use the Map/List conversion diff --git a/packages/flutter/lib/src/native/sentry_native_binding.dart b/packages/flutter/lib/src/native/sentry_native_binding.dart index 7f4b7b0bd7..2fe9ea03a9 100644 --- a/packages/flutter/lib/src/native/sentry_native_binding.dart +++ b/packages/flutter/lib/src/native/sentry_native_binding.dart @@ -102,4 +102,7 @@ abstract class SentryNativeBinding { /// Registers the active Dart trace ID with native replay. FutureOr registerTraceId(SentryId traceId); + + /// Registers a segment name with native replay for the current replay segment. + FutureOr registerSegmentName(String segmentName); } diff --git a/packages/flutter/lib/src/native/sentry_native_channel.dart b/packages/flutter/lib/src/native/sentry_native_channel.dart index 2867a9e1ce..3f50996a65 100644 --- a/packages/flutter/lib/src/native/sentry_native_channel.dart +++ b/packages/flutter/lib/src/native/sentry_native_channel.dart @@ -445,4 +445,9 @@ class SentryNativeChannel FutureOr registerTraceId(SentryId traceId) { // No-op. Replay trace ID registration is currently Android-only. } + + @override + FutureOr registerSegmentName(String segmentName) { + // No-op. Replay segment name registration is currently Android-only. + } } diff --git a/packages/flutter/lib/src/web/sentry_web.dart b/packages/flutter/lib/src/web/sentry_web.dart index 5554f101b5..f3a50a0397 100644 --- a/packages/flutter/lib/src/web/sentry_web.dart +++ b/packages/flutter/lib/src/web/sentry_web.dart @@ -271,6 +271,11 @@ class SentryWeb with SentryNativeSafeInvoker implements SentryNativeBinding { // No-op. Replay trace ID registration is currently Android-only. } + @override + FutureOr registerSegmentName(String segmentName) { + // No-op. Replay segment name registration is currently Android-only. + } + @override int? startProfiler(SentryId traceId) { _logNotSupported('start profiler'); diff --git a/packages/flutter/test/integrations/replay_telemetry_integration_test.dart b/packages/flutter/test/integrations/replay_telemetry_integration_test.dart index cd525f1fbe..5fcc55d980 100644 --- a/packages/flutter/test/integrations/replay_telemetry_integration_test.dart +++ b/packages/flutter/test/integrations/replay_telemetry_integration_test.dart @@ -117,6 +117,31 @@ void main() { verifyNever(fixture.nativeBinding.registerTraceId(any)); }); + + test('registers segment name with native replay', () async { + fixture.options.replay.sessionSampleRate = 0.5; + + await fixture.getSut().call(fixture.hub, fixture.options); + clearInteractions(fixture.nativeBinding); + final span = fixture.createTestSpan(name: 'CheckoutScreen'); + await fixture.options.lifecycleRegistry + .dispatchCallback(OnProcessSpan(span)); + + verify(fixture.nativeBinding.registerSegmentName('CheckoutScreen')) + .called(1); + }); + + test('does not register segment name for child spans', () async { + fixture.options.replay.sessionSampleRate = 0.5; + + await fixture.getSut().call(fixture.hub, fixture.options); + clearInteractions(fixture.nativeBinding); + final span = fixture.createChildTestSpan(); + await fixture.options.lifecycleRegistry + .dispatchCallback(OnProcessSpan(span)); + + verifyNever(fixture.nativeBinding.registerSegmentName(any)); + }); }); group('in session mode', () { @@ -346,6 +371,21 @@ void main() { verifyNever(fixture.nativeBinding.registerTraceId(any)); }); + + test('does not register segment name with native replay', () async { + fixture.options.replay.sessionSampleRate = 0.5; + + final sut = fixture.getSut(); + await sut.call(fixture.hub, fixture.options); + await sut.close(); + clearInteractions(fixture.nativeBinding); + + final span = fixture.createTestSpan(name: 'CheckoutScreen'); + await fixture.options.lifecycleRegistry + .dispatchCallback(OnProcessSpan(span)); + + verifyNever(fixture.nativeBinding.registerSegmentName(any)); + }); }); }); } @@ -370,6 +410,7 @@ class Fixture { }); when(nativeBinding.replayId).thenReturn(null); when(nativeBinding.registerTraceId(any)).thenReturn(null); + when(nativeBinding.registerSegmentName(any)).thenReturn(null); } SentryLog createTestLog() => SentryLog( @@ -388,8 +429,9 @@ class Fixture { attributes: {}, ); - RecordingSentrySpanV2 createTestSpan() => RecordingSentrySpanV2.root( - name: 'test-span', + RecordingSentrySpanV2 createTestSpan({String name = 'test-span'}) => + RecordingSentrySpanV2.root( + name: name, traceId: SentryId.newId(), onSpanEnd: (_) async {}, clock: options.clock, diff --git a/packages/flutter/test/mocks.mocks.dart b/packages/flutter/test/mocks.mocks.dart index b836c7d325..20fbea3923 100644 --- a/packages/flutter/test/mocks.mocks.dart +++ b/packages/flutter/test/mocks.mocks.dart @@ -2103,6 +2103,13 @@ class MockSentryNativeBinding extends _i1.Mock #registerTraceId, [traceId], )) as _i12.FutureOr); + + @override + _i12.FutureOr registerSegmentName(String? segmentName) => + (super.noSuchMethod(Invocation.method( + #registerSegmentName, + [segmentName], + )) as _i12.FutureOr); } /// A class which mocks [SentryDelayedFramesTracker].