From 151a549139e13c98ad940670f1c2fcabb0093acb Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 16 Jul 2026 14:58:50 +0200 Subject: [PATCH 1/3] feat(replay): Record segment names on Android Register streaming segment-root names with native replay so replay events include segment_names for Transaction Summary. Uses sentry-android 8.49.0 ReplayController.registerSegmentName. Fixes #3865 Co-Authored-By: Cursor Grok 4.5 Co-authored-by: Cursor --- .../replay_telemetry_integration.dart | 5 ++ .../lib/src/native/c/sentry_native.dart | 5 ++ .../src/native/java/sentry_native_java.dart | 16 +++++ .../lib/src/native/sentry_native_binding.dart | 3 + .../lib/src/native/sentry_native_channel.dart | 5 ++ packages/flutter/lib/src/web/sentry_web.dart | 5 ++ .../replay_telemetry_integration_test.dart | 58 ++++++++++++++++++- packages/flutter/test/mocks.mocks.dart | 7 +++ 8 files changed, 102 insertions(+), 2 deletions(-) diff --git a/packages/flutter/lib/src/integrations/replay_telemetry_integration.dart b/packages/flutter/lib/src/integrations/replay_telemetry_integration.dart index 9c8b229364..cc528f33e4 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) && span.name.isNotEmpty) { + 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..9f7a5c36c1 100644 --- a/packages/flutter/test/integrations/replay_telemetry_integration_test.dart +++ b/packages/flutter/test/integrations/replay_telemetry_integration_test.dart @@ -117,6 +117,43 @@ 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)); + }); + + test('does not register empty segment name', () async { + fixture.options.replay.sessionSampleRate = 0.5; + + await fixture.getSut().call(fixture.hub, fixture.options); + clearInteractions(fixture.nativeBinding); + final span = fixture.createTestSpan(name: ''); + await fixture.options.lifecycleRegistry + .dispatchCallback(OnProcessSpan(span)); + + verifyNever(fixture.nativeBinding.registerSegmentName(any)); + }); }); group('in session mode', () { @@ -346,6 +383,21 @@ void main() { verifyNever(fixture.nativeBinding.registerTraceId(any)); }); + + test('removes segment name registration on close', () 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 +422,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 +441,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]. From e6cedf56dcabccdf5d5ef4d5ea9be2e60fdd890a Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 16 Jul 2026 15:09:53 +0200 Subject: [PATCH 2/3] ref(replay): Drop empty segment name guard Empty names are already skipped in the Android JNI wrapper. Co-Authored-By: Cursor Grok 4.5 Co-authored-by: Cursor --- .../integrations/replay_telemetry_integration.dart | 2 +- .../replay_telemetry_integration_test.dart | 12 ------------ 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/packages/flutter/lib/src/integrations/replay_telemetry_integration.dart b/packages/flutter/lib/src/integrations/replay_telemetry_integration.dart index cc528f33e4..681b4d757a 100644 --- a/packages/flutter/lib/src/integrations/replay_telemetry_integration.dart +++ b/packages/flutter/lib/src/integrations/replay_telemetry_integration.dart @@ -63,7 +63,7 @@ class ReplayTelemetryIntegration implements Integration { } final span = event.span; - if (identical(span, span.segmentSpan) && span.name.isNotEmpty) { + if (identical(span, span.segmentSpan)) { return _native?.registerSegmentName(span.name); } }; diff --git a/packages/flutter/test/integrations/replay_telemetry_integration_test.dart b/packages/flutter/test/integrations/replay_telemetry_integration_test.dart index 9f7a5c36c1..d6d35cb4f2 100644 --- a/packages/flutter/test/integrations/replay_telemetry_integration_test.dart +++ b/packages/flutter/test/integrations/replay_telemetry_integration_test.dart @@ -142,18 +142,6 @@ void main() { verifyNever(fixture.nativeBinding.registerSegmentName(any)); }); - - test('does not register empty segment name', () async { - fixture.options.replay.sessionSampleRate = 0.5; - - await fixture.getSut().call(fixture.hub, fixture.options); - clearInteractions(fixture.nativeBinding); - final span = fixture.createTestSpan(name: ''); - await fixture.options.lifecycleRegistry - .dispatchCallback(OnProcessSpan(span)); - - verifyNever(fixture.nativeBinding.registerSegmentName(any)); - }); }); group('in session mode', () { From 0707155c7504fe8545a0238d3507f8403ed2e0ac Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 16 Jul 2026 16:00:43 +0200 Subject: [PATCH 3/3] test(replay): Clarify closed segment name test name Co-Authored-By: Cursor Grok 4.5 Co-authored-by: Cursor --- .../test/integrations/replay_telemetry_integration_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter/test/integrations/replay_telemetry_integration_test.dart b/packages/flutter/test/integrations/replay_telemetry_integration_test.dart index d6d35cb4f2..5fcc55d980 100644 --- a/packages/flutter/test/integrations/replay_telemetry_integration_test.dart +++ b/packages/flutter/test/integrations/replay_telemetry_integration_test.dart @@ -372,7 +372,7 @@ void main() { verifyNever(fixture.nativeBinding.registerTraceId(any)); }); - test('removes segment name registration on close', () async { + test('does not register segment name with native replay', () async { fixture.options.replay.sessionSampleRate = 0.5; final sut = fixture.getSut();