From 27ba53de96a6f034bcff759972222434a33ab435 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 11 Feb 2026 11:31:25 +0100 Subject: [PATCH 01/18] feat: Synchronize PropagationContext to native SDKs (#3406) Native crashes/errors on Android and iOS carry their own independently- generated traceId, disconnecting them from the Dart-side trace. This wires up the existing native setTrace APIs so the Dart PropagationContext is synced to native on init and whenever generateNewTrace() is called. - Add OnTraceReset lifecycle event dispatched from Hub.generateNewTrace() - Add NativeTraceSyncIntegration that subscribes to OnTraceReset and calls the platform-specific native setTrace API - Implement setTrace on all platform bindings (JNI for Android, method channel for Cocoa, no-op for C/Web) - Add supportsTraceSync capability flag to SentryNativeBinding - Disable native auto trace ID generation on Android so Flutter is the single source of truth - Add setTrace handler in iOS SentryFlutterPlugin.swift - Register NativeTraceSyncIntegration in default integrations Co-Authored-By: Claude Opus 4.6 --- packages/dart/lib/src/hub.dart | 3 + .../dart/lib/src/sdk_lifecycle_hooks.dart | 8 ++ packages/dart/test/hub_test.dart | 14 +++ .../sentry_flutter/SentryFlutterPlugin.swift | 17 +++ .../native_trace_sync_integration.dart | 38 ++++++ .../lib/src/native/c/sentry_native.dart | 7 ++ .../src/native/java/sentry_native_java.dart | 19 +++ .../native/java/sentry_native_java_init.dart | 1 + .../lib/src/native/sentry_native_binding.dart | 7 ++ .../lib/src/native/sentry_native_channel.dart | 20 ++++ packages/flutter/lib/src/sentry_flutter.dart | 4 + packages/flutter/lib/src/web/sentry_web.dart | 7 ++ packages/flutter/test/mocks.mocks.dart | 110 +++++++++++------- 13 files changed, 211 insertions(+), 44 deletions(-) create mode 100644 packages/flutter/lib/src/integrations/native_trace_sync_integration.dart diff --git a/packages/dart/lib/src/hub.dart b/packages/dart/lib/src/hub.dart index 8ffc57d15b..7ee07b464e 100644 --- a/packages/dart/lib/src/hub.dart +++ b/packages/dart/lib/src/hub.dart @@ -610,6 +610,9 @@ class Hub { // Create a brand-new trace and reset the sampling flag and sampleRand so // that the next root transaction can set it again. scope.propagationContext.resetTrace(); + // Fire-and-forget — don't await, matches current void return type. + _options.lifecycleRegistry + .dispatchCallback(OnTraceReset(scope.propagationContext)); } /// Gets the current active transaction or span. diff --git a/packages/dart/lib/src/sdk_lifecycle_hooks.dart b/packages/dart/lib/src/sdk_lifecycle_hooks.dart index 36912dc523..4c28626396 100644 --- a/packages/dart/lib/src/sdk_lifecycle_hooks.dart +++ b/packages/dart/lib/src/sdk_lifecycle_hooks.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:meta/meta.dart'; import '../sentry.dart'; +import 'propagation_context.dart'; @internal typedef SdkLifecycleCallback = FutureOr @@ -103,3 +104,10 @@ class OnProcessMetric extends SdkLifecycleEvent { OnProcessMetric(this.metric); } + +/// Dispatched when a new trace is generated via [Hub.generateNewTrace]. +@internal +class OnTraceReset extends SdkLifecycleEvent { + OnTraceReset(this.propagationContext); + final PropagationContext propagationContext; +} diff --git a/packages/dart/test/hub_test.dart b/packages/dart/test/hub_test.dart index a5dcc6e1c5..1627f3faa4 100644 --- a/packages/dart/test/hub_test.dart +++ b/packages/dart/test/hub_test.dart @@ -637,6 +637,20 @@ void main() { final newSampleRand = hub.scope.propagationContext.sampleRand; expect(newSampleRand, isNull); }); + + test('generateNewTrace dispatches OnTraceReset with propagation context', + () { + PropagationContext? receivedContext; + hub.options.lifecycleRegistry + .registerCallback((event) { + receivedContext = event.propagationContext; + }); + + hub.generateNewTrace(); + + expect(receivedContext, isNotNull); + expect(receivedContext, hub.scope.propagationContext); + }); }); group('Hub scope callback', () { diff --git a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift index e2b370d29e..7ac48d201e 100644 --- a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift +++ b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift @@ -161,6 +161,12 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { result(nil) #endif + case "setTrace": + let arguments = call.arguments as? [String: Any?] + let traceId = arguments?["traceId"] as? String + let spanId = arguments?["spanId"] as? String + setTrace(traceId: traceId, spanId: spanId, result: result) + default: result(FlutterMethodNotImplemented) } @@ -696,6 +702,17 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { result("") } + private func setTrace(traceId: String?, spanId: String?, result: @escaping FlutterResult) { + guard let traceId = traceId else { + result("") + return + } + let sentryTraceId = SentryId(uuidString: traceId) + let sentrySpanId = spanId != nil ? SpanId(value: spanId!) : nil + PrivateSentrySDKOnly.setTrace(sentryTraceId, spanId: sentrySpanId) + result("") + } + private func crash() { SentrySDK.crash() } diff --git a/packages/flutter/lib/src/integrations/native_trace_sync_integration.dart b/packages/flutter/lib/src/integrations/native_trace_sync_integration.dart new file mode 100644 index 0000000000..d3906adce8 --- /dev/null +++ b/packages/flutter/lib/src/integrations/native_trace_sync_integration.dart @@ -0,0 +1,38 @@ +// ignore_for_file: invalid_use_of_internal_member + +import 'package:meta/meta.dart'; + +import '../../sentry_flutter.dart'; +import '../native/sentry_native_binding.dart'; + +/// Synchronizes the Dart [PropagationContext] to the native SDK so that +/// native crashes/errors share the same trace as Dart events. +@internal +class NativeTraceSyncIntegration implements Integration { + static const integrationName = 'NativeTraceSync'; + final SentryNativeBinding _native; + SentryOptions? _options; + + NativeTraceSyncIntegration(this._native); + + @override + void call(Hub hub, SentryFlutterOptions options) { + options.lifecycleRegistry + .registerCallback(_syncTraceToNative); + options.sdk.addIntegration(integrationName); + + // Sync the initial PropagationContext created at Hub construction. + _syncTraceToNative(OnTraceReset(hub.scope.propagationContext)); + } + + @override + void close() { + _options?.lifecycleRegistry + .removeCallback(_syncTraceToNative); + } + + void _syncTraceToNative(OnTraceReset event) { + final ctx = event.propagationContext; + _native.setTrace(ctx.traceId, sampleRand: ctx.sampleRand); + } +} diff --git a/packages/flutter/lib/src/native/c/sentry_native.dart b/packages/flutter/lib/src/native/c/sentry_native.dart index 85c0b602c6..1f5dd4b1ae 100644 --- a/packages/flutter/lib/src/native/c/sentry_native.dart +++ b/packages/flutter/lib/src/native/c/sentry_native.dart @@ -310,6 +310,13 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { FutureOr updateSession({int? errors, String? status}) { _logNotSupported('updating session'); } + + @override + bool get supportsTraceSync => false; + + @override + FutureOr setTrace(SentryId traceId, + {SpanId? spanId, double? sampleRate, double? sampleRand}) {} } extension 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 5f56f35040..045915aa69 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -388,6 +388,25 @@ class SentryNativeJava extends SentryNativeChannel { replayConfig.release(); }); + + @override + bool get supportsTraceSync => true; + + @override + void setTrace(SentryId traceId, + {SpanId? spanId, double? sampleRate, double? sampleRand}) { + tryCatchSync('setTrace', () { + using((arena) { + final jTraceId = traceId.toString().toJString()..releasedBy(arena); + final jSpanId = (spanId ?? SpanId.newId()).toString().toJString() + ..releasedBy(arena); + final jSampleRate = sampleRate?.toJDouble()?..releasedBy(arena); + final jSampleRand = sampleRand?.toJDouble()?..releasedBy(arena); + native.InternalSentrySdk.setTrace( + jTraceId, jSpanId, jSampleRate, jSampleRand); + }); + }); + } } @visibleForTesting diff --git a/packages/flutter/lib/src/native/java/sentry_native_java_init.dart b/packages/flutter/lib/src/native/java/sentry_native_java_init.dart index 98af364019..479a98098b 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java_init.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java_init.dart @@ -239,6 +239,7 @@ void configureAndroidOptions({ } androidOptions.setSendDefaultPii(options.sendDefaultPii); androidOptions.setEnableScopeSync(options.enableNdkScopeSync); + androidOptions.setEnableAutoTraceIdGeneration(false); androidOptions .setProguardUuid(options.proguardUuid?.toJString()?..releasedBy(arena)); androidOptions.setEnableSpotlight(options.spotlight.enabled); diff --git a/packages/flutter/lib/src/native/sentry_native_binding.dart b/packages/flutter/lib/src/native/sentry_native_binding.dart index bf45423d25..5a8eeeaa0c 100644 --- a/packages/flutter/lib/src/native/sentry_native_binding.dart +++ b/packages/flutter/lib/src/native/sentry_native_binding.dart @@ -93,4 +93,11 @@ abstract class SentryNativeBinding { /// /// NNote: This is used on web platforms and is a no-op on non-web. FutureOr captureSession(); + + /// Whether the native SDK supports syncing the trace id. + bool get supportsTraceSync; + + /// Sets the trace context on the native SDK scope. + FutureOr setTrace(SentryId traceId, + {SpanId? spanId, double? sampleRate, double? sampleRand}); } diff --git a/packages/flutter/lib/src/native/sentry_native_channel.dart b/packages/flutter/lib/src/native/sentry_native_channel.dart index 10726fdc3a..5515edee58 100644 --- a/packages/flutter/lib/src/native/sentry_native_channel.dart +++ b/packages/flutter/lib/src/native/sentry_native_channel.dart @@ -422,4 +422,24 @@ class SentryNativeChannel FutureOr updateSession({int? errors, String? status}) { _logNotSupported('updating session'); } + + // Android handles supportings trace sync via JNI, not method channels. + @override + bool get supportsTraceSync => !options.platform.isAndroid; + + @override + FutureOr setTrace(SentryId traceId, + {SpanId? spanId, double? sampleRate, double? sampleRand}) { + if (options.platform.isAndroid) { + assert(false, + 'setTrace should not be used through method channels on Android.'); + return null; + } + return channel.invokeMethod('setTrace', { + 'traceId': traceId.toString(), + if (spanId != null) 'spanId': spanId.toString(), + if (sampleRate != null) 'sampleRate': sampleRate, + if (sampleRand != null) 'sampleRand': sampleRand, + }); + } } diff --git a/packages/flutter/lib/src/sentry_flutter.dart b/packages/flutter/lib/src/sentry_flutter.dart index 3ec4d43cd9..1c51b0666e 100644 --- a/packages/flutter/lib/src/sentry_flutter.dart +++ b/packages/flutter/lib/src/sentry_flutter.dart @@ -25,6 +25,7 @@ import 'integrations/native_app_start_handler.dart'; import 'integrations/replay_telemetry_integration.dart'; import 'integrations/screenshot_integration.dart'; import 'integrations/generic_app_start_integration.dart'; +import 'integrations/native_trace_sync_integration.dart'; import 'integrations/thread_info_integration.dart'; import 'integrations/web_session_integration.dart'; import 'native/factory.dart'; @@ -189,6 +190,9 @@ mixin SentryFlutter { // We also need to call this before the native sdk integrations so release is properly propagated. integrations.add(LoadReleaseIntegration()); integrations.add(createSdkIntegration(native)); + if (native.supportsTraceSync) { + integrations.add(NativeTraceSyncIntegration(native)); + } integrations.add(createLoadDebugImagesIntegration(native)); if (!platform.isWeb) { if (native.supportsLoadContexts) { diff --git a/packages/flutter/lib/src/web/sentry_web.dart b/packages/flutter/lib/src/web/sentry_web.dart index 05fe22e09b..52afef5ec3 100644 --- a/packages/flutter/lib/src/web/sentry_web.dart +++ b/packages/flutter/lib/src/web/sentry_web.dart @@ -275,6 +275,13 @@ class SentryWeb with SentryNativeSafeInvoker implements SentryNativeBinding { @override SentryId? get replayId => null; + @override + bool get supportsTraceSync => false; + + @override + FutureOr setTrace(SentryId traceId, + {SpanId? spanId, double? sampleRate, double? sampleRand}) {} + @override SentryFlutterOptions get options => _options; } diff --git a/packages/flutter/test/mocks.mocks.dart b/packages/flutter/test/mocks.mocks.dart index db45428c78..19d42d5f94 100644 --- a/packages/flutter/test/mocks.mocks.dart +++ b/packages/flutter/test/mocks.mocks.dart @@ -4,14 +4,14 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i12; -import 'dart:developer' as _i23; -import 'dart:typed_data' as _i19; +import 'dart:developer' as _i22; +import 'dart:typed_data' as _i18; import 'dart:ui' as _i6; import 'package:flutter/foundation.dart' as _i8; import 'package:flutter/gestures.dart' as _i7; import 'package:flutter/rendering.dart' as _i10; -import 'package:flutter/scheduler.dart' as _i22; +import 'package:flutter/scheduler.dart' as _i21; import 'package:flutter/services.dart' as _i4; import 'package:flutter/src/widgets/_window.dart' as _i11; import 'package:flutter/src/widgets/binding.dart' as _i5; @@ -21,19 +21,18 @@ import 'package:mockito/mockito.dart' as _i1; import 'package:mockito/src/dummies.dart' as _i15; import 'package:sentry/src/profiling.dart' as _i16; import 'package:sentry/src/sentry_tracer.dart' as _i3; -import 'package:sentry/src/telemetry/metric/metric.dart' as _i17; import 'package:sentry_flutter/sentry_flutter.dart' as _i2; import 'package:sentry_flutter/src/frames_tracking/sentry_delayed_frames_tracker.dart' - as _i21; -import 'package:sentry_flutter/src/native/sentry_native_binding.dart' as _i18; + as _i20; +import 'package:sentry_flutter/src/native/sentry_native_binding.dart' as _i17; import 'package:sentry_flutter/src/navigation/time_to_display_tracker.dart' - as _i25; + as _i24; import 'package:sentry_flutter/src/navigation/time_to_full_display_tracker.dart' - as _i27; -import 'package:sentry_flutter/src/navigation/time_to_initial_display_tracker.dart' as _i26; -import 'package:sentry_flutter/src/replay/replay_config.dart' as _i20; -import 'package:sentry_flutter/src/web/sentry_js_binding.dart' as _i24; +import 'package:sentry_flutter/src/navigation/time_to_initial_display_tracker.dart' + as _i25; +import 'package:sentry_flutter/src/replay/replay_config.dart' as _i19; +import 'package:sentry_flutter/src/web/sentry_js_binding.dart' as _i23; import 'mocks.dart' as _i14; @@ -1767,7 +1766,7 @@ class MockSentryClient extends _i1.Mock implements _i2.SentryClient { @override _i12.Future captureMetric( - _i17.SentryMetric? metric, { + _i2.SentryMetric? metric, { _i2.Scope? scope, }) => (super.noSuchMethod( @@ -1880,7 +1879,7 @@ class MockMethodChannel extends _i1.Mock implements _i4.MethodChannel { /// /// See the documentation for Mockito's code generation for more information. class MockSentryNativeBinding extends _i1.Mock - implements _i18.SentryNativeBinding { + implements _i17.SentryNativeBinding { MockSentryNativeBinding() { _i1.throwOnMissingStub(this); } @@ -1903,6 +1902,12 @@ class MockSentryNativeBinding extends _i1.Mock returnValue: false, ) as bool); + @override + bool get supportsTraceSync => (super.noSuchMethod( + Invocation.getter(#supportsTraceSync), + returnValue: false, + ) as bool); + @override _i12.FutureOr init(_i2.Hub? hub) => (super.noSuchMethod(Invocation.method( @@ -1912,7 +1917,7 @@ class MockSentryNativeBinding extends _i1.Mock @override _i12.FutureOr captureEnvelope( - _i19.Uint8List? envelopeData, + _i18.Uint8List? envelopeData, bool? containsUnhandledException, ) => (super.noSuchMethod(Invocation.method( @@ -2035,7 +2040,7 @@ class MockSentryNativeBinding extends _i1.Mock )) as _i12.FutureOr?>); @override - _i12.FutureOr setReplayConfig(_i20.ReplayConfig? config) => + _i12.FutureOr setReplayConfig(_i19.ReplayConfig? config) => (super.noSuchMethod(Invocation.method( #setReplayConfig, [config], @@ -2063,25 +2068,42 @@ class MockSentryNativeBinding extends _i1.Mock [], {#ignoreDuration: ignoreDuration}, )) as _i12.FutureOr); + + @override + _i12.FutureOr setTrace( + _i2.SentryId? traceId, { + _i2.SpanId? spanId, + double? sampleRate, + double? sampleRand, + }) => + (super.noSuchMethod(Invocation.method( + #setTrace, + [traceId], + { + #spanId: spanId, + #sampleRate: sampleRate, + #sampleRand: sampleRand, + }, + )) as _i12.FutureOr); } /// A class which mocks [SentryDelayedFramesTracker]. /// /// See the documentation for Mockito's code generation for more information. class MockSentryDelayedFramesTracker extends _i1.Mock - implements _i21.SentryDelayedFramesTracker { + implements _i20.SentryDelayedFramesTracker { MockSentryDelayedFramesTracker() { _i1.throwOnMissingStub(this); } @override - List<_i21.SentryFrameTiming> get delayedFrames => (super.noSuchMethod( + List<_i20.SentryFrameTiming> get delayedFrames => (super.noSuchMethod( Invocation.getter(#delayedFrames), - returnValue: <_i21.SentryFrameTiming>[], - ) as List<_i21.SentryFrameTiming>); + returnValue: <_i20.SentryFrameTiming>[], + ) as List<_i20.SentryFrameTiming>); @override - List<_i21.SentryFrameTiming> getFramesIntersecting({ + List<_i20.SentryFrameTiming> getFramesIntersecting({ required DateTime? startTimestamp, required DateTime? endTimestamp, }) => @@ -2094,8 +2116,8 @@ class MockSentryDelayedFramesTracker extends _i1.Mock #endTimestamp: endTimestamp, }, ), - returnValue: <_i21.SentryFrameTiming>[], - ) as List<_i21.SentryFrameTiming>); + returnValue: <_i20.SentryFrameTiming>[], + ) as List<_i20.SentryFrameTiming>); @override void addDelayedFrame( @@ -2124,7 +2146,7 @@ class MockSentryDelayedFramesTracker extends _i1.Mock ); @override - _i21.SpanFrameMetrics? getFrameMetrics({ + _i20.SpanFrameMetrics? getFrameMetrics({ required DateTime? spanStartTimestamp, required DateTime? spanEndTimestamp, }) => @@ -2135,7 +2157,7 @@ class MockSentryDelayedFramesTracker extends _i1.Mock #spanStartTimestamp: spanStartTimestamp, #spanEndTimestamp: spanEndTimestamp, }, - )) as _i21.SpanFrameMetrics?); + )) as _i20.SpanFrameMetrics?); @override void clear() => super.noSuchMethod( @@ -2274,14 +2296,14 @@ class MockWidgetsFlutterBinding extends _i1.Mock ); @override - _i22.SchedulingStrategy get schedulingStrategy => (super.noSuchMethod( + _i21.SchedulingStrategy get schedulingStrategy => (super.noSuchMethod( Invocation.getter(#schedulingStrategy), returnValue: ({ required int priority, - required _i22.SchedulerBinding scheduler, + required _i21.SchedulerBinding scheduler, }) => false, - ) as _i22.SchedulingStrategy); + ) as _i21.SchedulingStrategy); @override int get transientCallbackCount => (super.noSuchMethod( @@ -2302,10 +2324,10 @@ class MockWidgetsFlutterBinding extends _i1.Mock ) as bool); @override - _i22.SchedulerPhase get schedulerPhase => (super.noSuchMethod( + _i21.SchedulerPhase get schedulerPhase => (super.noSuchMethod( Invocation.getter(#schedulerPhase), - returnValue: _i22.SchedulerPhase.idle, - ) as _i22.SchedulerPhase); + returnValue: _i21.SchedulerPhase.idle, + ) as _i21.SchedulerPhase); @override bool get framesEnabled => (super.noSuchMethod( @@ -2332,7 +2354,7 @@ class MockWidgetsFlutterBinding extends _i1.Mock ) as Duration); @override - set schedulingStrategy(_i22.SchedulingStrategy? value) => super.noSuchMethod( + set schedulingStrategy(_i21.SchedulingStrategy? value) => super.noSuchMethod( Invocation.setter( #schedulingStrategy, value, @@ -2929,10 +2951,10 @@ class MockWidgetsFlutterBinding extends _i1.Mock @override _i12.Future scheduleTask( - _i22.TaskCallback? task, - _i22.Priority? priority, { + _i21.TaskCallback? task, + _i21.Priority? priority, { String? debugLabel, - _i23.Flow? flow, + _i22.Flow? flow, }) => (super.noSuchMethod( Invocation.method( @@ -2990,7 +3012,7 @@ class MockWidgetsFlutterBinding extends _i1.Mock @override int scheduleFrameCallback( - _i22.FrameCallback? callback, { + _i21.FrameCallback? callback, { bool? rescheduling = false, bool? scheduleNewFrame = true, }) => @@ -3044,7 +3066,7 @@ class MockWidgetsFlutterBinding extends _i1.Mock ) as bool); @override - void addPersistentFrameCallback(_i22.FrameCallback? callback) => + void addPersistentFrameCallback(_i21.FrameCallback? callback) => super.noSuchMethod( Invocation.method( #addPersistentFrameCallback, @@ -3055,7 +3077,7 @@ class MockWidgetsFlutterBinding extends _i1.Mock @override void addPostFrameCallback( - _i22.FrameCallback? callback, { + _i21.FrameCallback? callback, { String? debugLabel = 'callback', }) => super.noSuchMethod( @@ -3131,12 +3153,12 @@ class MockWidgetsFlutterBinding extends _i1.Mock ); @override - _i22.PerformanceModeRequestHandle? requestPerformanceMode( + _i21.PerformanceModeRequestHandle? requestPerformanceMode( _i6.DartPerformanceMode? mode) => (super.noSuchMethod(Invocation.method( #requestPerformanceMode, [mode], - )) as _i22.PerformanceModeRequestHandle?); + )) as _i21.PerformanceModeRequestHandle?); @override void handleDrawFrame() => super.noSuchMethod( @@ -3725,7 +3747,7 @@ class MockWidgetsFlutterBinding extends _i1.Mock /// A class which mocks [SentryJsBinding]. /// /// See the documentation for Mockito's code generation for more information. -class MockSentryJsBinding extends _i1.Mock implements _i24.SentryJsBinding { +class MockSentryJsBinding extends _i1.Mock implements _i23.SentryJsBinding { MockSentryJsBinding() { _i1.throwOnMissingStub(this); } @@ -3797,7 +3819,7 @@ class MockSentryJsBinding extends _i1.Mock implements _i24.SentryJsBinding { /// /// See the documentation for Mockito's code generation for more information. class MockTimeToDisplayTracker extends _i1.Mock - implements _i25.TimeToDisplayTracker { + implements _i24.TimeToDisplayTracker { MockTimeToDisplayTracker() { _i1.throwOnMissingStub(this); } @@ -3884,7 +3906,7 @@ class MockTimeToDisplayTracker extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockTimeToInitialDisplayTracker extends _i1.Mock - implements _i26.TimeToInitialDisplayTracker { + implements _i25.TimeToInitialDisplayTracker { MockTimeToInitialDisplayTracker() { _i1.throwOnMissingStub(this); } @@ -3920,7 +3942,7 @@ class MockTimeToInitialDisplayTracker extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockTimeToFullDisplayTracker extends _i1.Mock - implements _i27.TimeToFullDisplayTracker { + implements _i26.TimeToFullDisplayTracker { MockTimeToFullDisplayTracker() { _i1.throwOnMissingStub(this); } @@ -4160,7 +4182,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { )) as _i12.FutureOr); @override - _i12.Future captureMetric(_i17.SentryMetric? metric) => + _i12.Future captureMetric(_i2.SentryMetric? metric) => (super.noSuchMethod( Invocation.method( #captureMetric, From d5d5d97154b6945be978233ed1a8a2987ed8d1f3 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 11 Feb 2026 15:19:52 +0100 Subject: [PATCH 02/18] Fix build --- .../Sources/sentry_flutter/SentryFlutterPlugin.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift index 7ac48d201e..c394081516 100644 --- a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift +++ b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift @@ -708,7 +708,7 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { return } let sentryTraceId = SentryId(uuidString: traceId) - let sentrySpanId = spanId != nil ? SpanId(value: spanId!) : nil + let sentrySpanId = spanId.flatMap { SpanId(value: $0) } ?? SpanId() PrivateSentrySDKOnly.setTrace(sentryTraceId, spanId: sentrySpanId) result("") } From 41528cde1117e942a92a76740b732aff0db95ab7 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 12 Feb 2026 13:41:18 +0100 Subject: [PATCH 03/18] Update CHANGELOG --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01089dfba6..990006bbc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +### Features + +- Synchronize `traceId` to native SDKs ([#3507](https://github.com/getsentry/sentry-dart/pull/3507)) + - Native events (e.g. from Android or iOS) such as errors, logs, and spans now share the same trace as Dart events, enabling unified trace views across layers + ### Dependencies - Bump JavaScript SDK from v10.6.0 to v10.38.0 ([#3474](https://github.com/getsentry/sentry-dart/pull/3474)) @@ -14,7 +19,7 @@ - Bump Native SDK from v0.12.3 to v0.12.5 ([#3481](https://github.com/getsentry/sentry-dart/pull/3481)) - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0125) - - [diff](https://github.com/getsentry/sentry-native/compare/0.12.3...0.12.5) + - [diff](hgttps://github.com/getsentry/sentry-native/compare/0.12.3...0.12.5) - Bump Android SDK from v8.30.0 to v8.31.0 ([#3476](https://github.com/getsentry/sentry-dart/pull/3476)) - [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#8310) - [diff](https://github.com/getsentry/sentry-java/compare/8.30.0...8.31.0) From 83d779652f3a481fd783d5dd7c3fb2908ab8b284 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 12 Feb 2026 14:28:54 +0100 Subject: [PATCH 04/18] Update --- packages/dart/lib/src/hub.dart | 3 +- .../dart/lib/src/sdk_lifecycle_hooks.dart | 3 +- .../integration_test/integration_test.dart | 39 +++ packages/flutter/ffi-jni.yaml | 1 + .../native_trace_sync_integration.dart | 1 + .../flutter/lib/src/native/java/binding.dart | 300 +++++++++++++++++- .../native_trace_sync_integration_test.dart | 99 ++++++ 7 files changed, 440 insertions(+), 6 deletions(-) create mode 100644 packages/flutter/test/integrations/native_trace_sync_integration_test.dart diff --git a/packages/dart/lib/src/hub.dart b/packages/dart/lib/src/hub.dart index 7ee07b464e..1b1e0f155b 100644 --- a/packages/dart/lib/src/hub.dart +++ b/packages/dart/lib/src/hub.dart @@ -610,7 +610,8 @@ class Hub { // Create a brand-new trace and reset the sampling flag and sampleRand so // that the next root transaction can set it again. scope.propagationContext.resetTrace(); - // Fire-and-forget — don't await, matches current void return type. + // Fire-and-forget the callback + // Native SDK synchronization over async method channels may be slightly delayed, but this is not problematic in practice. _options.lifecycleRegistry .dispatchCallback(OnTraceReset(scope.propagationContext)); } diff --git a/packages/dart/lib/src/sdk_lifecycle_hooks.dart b/packages/dart/lib/src/sdk_lifecycle_hooks.dart index 4c28626396..37f11107f8 100644 --- a/packages/dart/lib/src/sdk_lifecycle_hooks.dart +++ b/packages/dart/lib/src/sdk_lifecycle_hooks.dart @@ -108,6 +108,7 @@ class OnProcessMetric extends SdkLifecycleEvent { /// Dispatched when a new trace is generated via [Hub.generateNewTrace]. @internal class OnTraceReset extends SdkLifecycleEvent { - OnTraceReset(this.propagationContext); final PropagationContext propagationContext; + + OnTraceReset(this.propagationContext); } diff --git a/packages/flutter/example/integration_test/integration_test.dart b/packages/flutter/example/integration_test/integration_test.dart index 4d7f9dce72..20e8334d7c 100644 --- a/packages/flutter/example/integration_test/integration_test.dart +++ b/packages/flutter/example/integration_test/integration_test.dart @@ -1065,6 +1065,45 @@ void main() { } }); + // We currently only test this on Android + // Setting up iOS for testing this is a big time effort so we rely on manually testing there for now + testWidgets('setTrace syncs Dart traceId to native Android scope', + (tester) async { + await restoreFlutterOnErrorAfter(() async { + await setupSentryAndApp(tester); + }); + + final dartTraceId = + Sentry.currentHub.scope.propagationContext.traceId.toString(); + + final traceParent = jni.Sentry.getTraceparent(); + expect(traceParent, isNotNull, + reason: 'Native traceparent should not be null'); + final traceHeader = traceParent!.getValue().toDartString(); + + final nativeTraceId = traceHeader.split('-').first; + expect(nativeTraceId, dartTraceId, + reason: 'Native traceId should match Dart traceId after initial sync'); + + Sentry.currentHub.generateNewTrace(); + final newDartTraceId = + Sentry.currentHub.scope.propagationContext.traceId.toString(); + expect(newDartTraceId, isNot(dartTraceId), + reason: 'New trace should have a different traceId'); + + // Allow the fire-and-forget dispatch to complete + await Future.delayed(const Duration(milliseconds: 100)); + + final newTraceParent = + jni.Sentry.getTraceparent()?.getValue().toDartString(); + final newTraceHeader = newTraceParent!.toString(); + + final newNativeTraceId = newTraceHeader.split('-').first; + expect(newNativeTraceId, newDartTraceId, + reason: + 'Native traceId should match new Dart traceId after generateNewTrace'); + }, skip: !Platform.isAndroid); + group('e2e', () { var output = find.byKey(const Key('output')); late Fixture fixture; diff --git a/packages/flutter/ffi-jni.yaml b/packages/flutter/ffi-jni.yaml index 2a12b989c2..4a7a804a65 100644 --- a/packages/flutter/ffi-jni.yaml +++ b/packages/flutter/ffi-jni.yaml @@ -40,6 +40,7 @@ classes: - io.sentry.protocol.SentryPackage - io.sentry.rrweb.RRWebOptionsEvent - io.sentry.rrweb.RRWebEvent + - io.sentry.SentryTraceHeader - java.net.Proxy - android.graphics.Bitmap - android.content.Context diff --git a/packages/flutter/lib/src/integrations/native_trace_sync_integration.dart b/packages/flutter/lib/src/integrations/native_trace_sync_integration.dart index d3906adce8..2cde26671a 100644 --- a/packages/flutter/lib/src/integrations/native_trace_sync_integration.dart +++ b/packages/flutter/lib/src/integrations/native_trace_sync_integration.dart @@ -17,6 +17,7 @@ class NativeTraceSyncIntegration implements Integration { @override void call(Hub hub, SentryFlutterOptions options) { + _options = options; options.lifecycleRegistry .registerCallback(_syncTraceToNative); options.sdk.addIntegration(integrationName); diff --git a/packages/flutter/lib/src/native/java/binding.dart b/packages/flutter/lib/src/native/java/binding.dart index 5a4868e3dd..45265acf04 100644 --- a/packages/flutter/lib/src/native/java/binding.dart +++ b/packages/flutter/lib/src/native/java/binding.dart @@ -8181,10 +8181,10 @@ class Sentry extends jni$_.JObject { /// from: `static public io.sentry.SentryTraceHeader getTraceparent()` /// The returned object must be released after use, by calling the [release] method. - static jni$_.JObject? getTraceparent() { + static SentryTraceHeader? getTraceparent() { return _getTraceparent( _class.reference.pointer, _id_getTraceparent as jni$_.JMethodIDPtr) - .object(const jni$_.JObjectNullableType()); + .object(const $SentryTraceHeader$NullableType()); } static final _id_getBaggage = _class.staticMethodId( @@ -31458,10 +31458,10 @@ class ScopesAdapter extends jni$_.JObject { /// from: `public io.sentry.SentryTraceHeader getTraceparent()` /// The returned object must be released after use, by calling the [release] method. - jni$_.JObject? getTraceparent() { + SentryTraceHeader? getTraceparent() { return _getTraceparent( reference.pointer, _id_getTraceparent as jni$_.JMethodIDPtr) - .object(const jni$_.JObjectNullableType()); + .object(const $SentryTraceHeader$NullableType()); } static final _id_getBaggage = _class.instanceMethodId( @@ -38652,6 +38652,298 @@ final class $RRWebEvent$Type extends jni$_.JObjType { } } +/// from: `io.sentry.SentryTraceHeader` +class SentryTraceHeader extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JObjType $type; + + @jni$_.internal + SentryTraceHeader.fromReference( + jni$_.JReference reference, + ) : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName(r'io/sentry/SentryTraceHeader'); + + /// The type which includes information such as the signature of this class. + static const nullableType = $SentryTraceHeader$NullableType(); + static const type = $SentryTraceHeader$Type(); + static final _id_SENTRY_TRACE_HEADER = _class.staticFieldId( + r'SENTRY_TRACE_HEADER', + r'Ljava/lang/String;', + ); + + /// from: `static public final java.lang.String SENTRY_TRACE_HEADER` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JString? get SENTRY_TRACE_HEADER => + _id_SENTRY_TRACE_HEADER.get(_class, const jni$_.JStringNullableType()); + + static final _id_new$ = _class.constructorId( + r'(Lio/sentry/protocol/SentryId;Lio/sentry/SpanId;Ljava/lang/Boolean;)V', + ); + + static final _new$ = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer + )>)>>('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer)>(); + + /// from: `public void (io.sentry.protocol.SentryId sentryId, io.sentry.SpanId spanId, java.lang.Boolean boolean)` + /// The returned object must be released after use, by calling the [release] method. + factory SentryTraceHeader( + SentryId sentryId, + jni$_.JObject spanId, + jni$_.JBoolean? boolean, + ) { + final _$sentryId = sentryId.reference; + final _$spanId = spanId.reference; + final _$boolean = boolean?.reference ?? jni$_.jNullReference; + return SentryTraceHeader.fromReference(_new$( + _class.reference.pointer, + _id_new$ as jni$_.JMethodIDPtr, + _$sentryId.pointer, + _$spanId.pointer, + _$boolean.pointer) + .reference); + } + + static final _id_new$1 = _class.constructorId( + r'(Ljava/lang/String;)V', + ); + + static final _new$1 = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>)>>( + 'globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function(jni$_.Pointer, + jni$_.JMethodIDPtr, jni$_.Pointer)>(); + + /// from: `public void (java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + factory SentryTraceHeader.new$1( + jni$_.JString string, + ) { + final _$string = string.reference; + return SentryTraceHeader.fromReference(_new$1(_class.reference.pointer, + _id_new$1 as jni$_.JMethodIDPtr, _$string.pointer) + .reference); + } + + static final _id_getName = _class.instanceMethodId( + r'getName', + r'()Ljava/lang/String;', + ); + + static final _getName = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `public java.lang.String getName()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString getName() { + return _getName(reference.pointer, _id_getName as jni$_.JMethodIDPtr) + .object(const jni$_.JStringType()); + } + + static final _id_getValue = _class.instanceMethodId( + r'getValue', + r'()Ljava/lang/String;', + ); + + static final _getValue = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `public java.lang.String getValue()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString getValue() { + return _getValue(reference.pointer, _id_getValue as jni$_.JMethodIDPtr) + .object(const jni$_.JStringType()); + } + + static final _id_getTraceId = _class.instanceMethodId( + r'getTraceId', + r'()Lio/sentry/protocol/SentryId;', + ); + + static final _getTraceId = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `public io.sentry.protocol.SentryId getTraceId()` + /// The returned object must be released after use, by calling the [release] method. + SentryId getTraceId() { + return _getTraceId(reference.pointer, _id_getTraceId as jni$_.JMethodIDPtr) + .object(const $SentryId$Type()); + } + + static final _id_getSpanId = _class.instanceMethodId( + r'getSpanId', + r'()Lio/sentry/SpanId;', + ); + + static final _getSpanId = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `public io.sentry.SpanId getSpanId()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JObject getSpanId() { + return _getSpanId(reference.pointer, _id_getSpanId as jni$_.JMethodIDPtr) + .object(const jni$_.JObjectType()); + } + + static final _id_isSampled = _class.instanceMethodId( + r'isSampled', + r'()Ljava/lang/Boolean;', + ); + + static final _isSampled = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `public java.lang.Boolean isSampled()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JBoolean? isSampled() { + return _isSampled(reference.pointer, _id_isSampled as jni$_.JMethodIDPtr) + .object(const jni$_.JBooleanNullableType()); + } +} + +final class $SentryTraceHeader$NullableType + extends jni$_.JObjType { + @jni$_.internal + const $SentryTraceHeader$NullableType(); + + @jni$_.internal + @core$_.override + String get signature => r'Lio/sentry/SentryTraceHeader;'; + + @jni$_.internal + @core$_.override + SentryTraceHeader? fromReference(jni$_.JReference reference) => + reference.isNull + ? null + : SentryTraceHeader.fromReference( + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JObjType get superType => const jni$_.JObjectNullableType(); + + @jni$_.internal + @core$_.override + jni$_.JObjType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($SentryTraceHeader$NullableType).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($SentryTraceHeader$NullableType) && + other is $SentryTraceHeader$NullableType; + } +} + +final class $SentryTraceHeader$Type extends jni$_.JObjType { + @jni$_.internal + const $SentryTraceHeader$Type(); + + @jni$_.internal + @core$_.override + String get signature => r'Lio/sentry/SentryTraceHeader;'; + + @jni$_.internal + @core$_.override + SentryTraceHeader fromReference(jni$_.JReference reference) => + SentryTraceHeader.fromReference( + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JObjType get superType => const jni$_.JObjectNullableType(); + + @jni$_.internal + @core$_.override + jni$_.JObjType get nullableType => + const $SentryTraceHeader$NullableType(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($SentryTraceHeader$Type).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($SentryTraceHeader$Type) && + other is $SentryTraceHeader$Type; + } +} + /// from: `java.net.Proxy$Type` class Proxy$Type extends jni$_.JObject { @jni$_.internal diff --git a/packages/flutter/test/integrations/native_trace_sync_integration_test.dart b/packages/flutter/test/integrations/native_trace_sync_integration_test.dart new file mode 100644 index 0000000000..cdf78496be --- /dev/null +++ b/packages/flutter/test/integrations/native_trace_sync_integration_test.dart @@ -0,0 +1,99 @@ +// ignore_for_file: invalid_use_of_internal_member + +@TestOn('vm') +library; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry_flutter/src/integrations/native_trace_sync_integration.dart'; +import 'package:sentry_flutter/src/native/sentry_native_binding.dart'; + +import '../mocks.dart'; + +void main() { + group(NativeTraceSyncIntegration, () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + test('adds integration', () { + fixture.registerIntegration(); + + expect(fixture.options.sdk.integrations, + contains(NativeTraceSyncIntegration.integrationName)); + }); + + test('syncs initial propagation context on registration', () { + fixture.registerIntegration(); + + final call = fixture.binding.setTraceCalls.single; + expect(call.traceId, fixture.hub.scope.propagationContext.traceId); + expect(call.sampleRand, isNull); + }); + + test('syncs trace when OnTraceReset is dispatched', () { + fixture.registerIntegration(); + fixture.binding.setTraceCalls.clear(); + + fixture.hub.generateNewTrace(); + + // We cannot assert that the trace propagated to native here + // instead we just assert that the call was made with the correct trace ID. + final call = fixture.binding.setTraceCalls.single; + expect(call.traceId, fixture.hub.scope.propagationContext.traceId); + }); + + test('passes sampleRand to native when set', () { + fixture.hub.scope.propagationContext.sampleRand = 0.42; + + fixture.registerIntegration(); + + final call = fixture.binding.setTraceCalls.single; + expect(call.traceId, fixture.hub.scope.propagationContext.traceId); + expect(call.sampleRand, 0.42); + }); + + test('unregisters callback on close', () { + fixture.registerIntegration(); + fixture.binding.setTraceCalls.clear(); + + fixture.sut.close(); + fixture.hub.generateNewTrace(); + + expect(fixture.binding.setTraceCalls, isEmpty); + }); + }); +} + +class Fixture { + final options = defaultTestOptions(); + final binding = _FakeNativeBinding(); + late final hub = Hub(options); + late final sut = NativeTraceSyncIntegration(binding); + + void registerIntegration() { + sut.call(hub, options); + } +} + +class _SetTraceCall { + final SentryId traceId; + final SpanId? spanId; + final double? sampleRate; + final double? sampleRand; + + _SetTraceCall(this.traceId, {this.spanId, this.sampleRate, this.sampleRand}); +} + +class _FakeNativeBinding extends Fake implements SentryNativeBinding { + final setTraceCalls = <_SetTraceCall>[]; + + @override + void setTrace(SentryId traceId, + {SpanId? spanId, double? sampleRate, double? sampleRand}) { + setTraceCalls.add(_SetTraceCall(traceId, + spanId: spanId, sampleRate: sampleRate, sampleRand: sampleRand)); + } +} From d417ff69bf0f68ebc1d110fe1683354ef1535956 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 12 Feb 2026 15:16:38 +0100 Subject: [PATCH 05/18] Update --- packages/dart/lib/src/hub.dart | 5 ++-- .../dart/lib/src/sdk_lifecycle_hooks.dart | 5 ++-- .../sentry_flutter/SentryFlutterPlugin.swift | 4 ++-- .../native_trace_sync_integration.dart | 8 ++++--- .../lib/src/native/c/sentry_native.dart | 5 ++-- .../src/native/java/sentry_native_java.dart | 11 +++------ .../lib/src/native/sentry_native_binding.dart | 3 +-- .../lib/src/native/sentry_native_channel.dart | 7 ++---- packages/flutter/lib/src/web/sentry_web.dart | 9 ++++---- .../native_trace_sync_integration_test.dart | 23 ++++--------------- packages/flutter/test/mocks.mocks.dart | 16 +++++-------- 11 files changed, 37 insertions(+), 59 deletions(-) diff --git a/packages/dart/lib/src/hub.dart b/packages/dart/lib/src/hub.dart index 1b1e0f155b..bf512874e3 100644 --- a/packages/dart/lib/src/hub.dart +++ b/packages/dart/lib/src/hub.dart @@ -612,8 +612,9 @@ class Hub { scope.propagationContext.resetTrace(); // Fire-and-forget the callback // Native SDK synchronization over async method channels may be slightly delayed, but this is not problematic in practice. - _options.lifecycleRegistry - .dispatchCallback(OnTraceReset(scope.propagationContext)); + _options.lifecycleRegistry.dispatchCallback(OnTraceReset( + scope.propagationContext.traceId, + getSpan()?.context.spanId ?? SpanId.newId())); } /// Gets the current active transaction or span. diff --git a/packages/dart/lib/src/sdk_lifecycle_hooks.dart b/packages/dart/lib/src/sdk_lifecycle_hooks.dart index 37f11107f8..1c82c2e7cd 100644 --- a/packages/dart/lib/src/sdk_lifecycle_hooks.dart +++ b/packages/dart/lib/src/sdk_lifecycle_hooks.dart @@ -108,7 +108,8 @@ class OnProcessMetric extends SdkLifecycleEvent { /// Dispatched when a new trace is generated via [Hub.generateNewTrace]. @internal class OnTraceReset extends SdkLifecycleEvent { - final PropagationContext propagationContext; + final SentryId traceId; + final SpanId spanId; - OnTraceReset(this.propagationContext); + OnTraceReset(this.traceId, this.spanId); } diff --git a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift index c394081516..7e3116a4f4 100644 --- a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift +++ b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift @@ -703,12 +703,12 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { } private func setTrace(traceId: String?, spanId: String?, result: @escaping FlutterResult) { - guard let traceId = traceId else { + guard let traceId = traceId, let spanId = spanId else { result("") return } let sentryTraceId = SentryId(uuidString: traceId) - let sentrySpanId = spanId.flatMap { SpanId(value: $0) } ?? SpanId() + let sentrySpanId = SpanId(value: spanId) PrivateSentrySDKOnly.setTrace(sentryTraceId, spanId: sentrySpanId) result("") } diff --git a/packages/flutter/lib/src/integrations/native_trace_sync_integration.dart b/packages/flutter/lib/src/integrations/native_trace_sync_integration.dart index 2cde26671a..c7b56c7e70 100644 --- a/packages/flutter/lib/src/integrations/native_trace_sync_integration.dart +++ b/packages/flutter/lib/src/integrations/native_trace_sync_integration.dart @@ -22,8 +22,11 @@ class NativeTraceSyncIntegration implements Integration { .registerCallback(_syncTraceToNative); options.sdk.addIntegration(integrationName); + final traceId = hub.scope.propagationContext.traceId; + final spanId = hub.getSpan()?.context.spanId ?? SpanId.newId(); + // Sync the initial PropagationContext created at Hub construction. - _syncTraceToNative(OnTraceReset(hub.scope.propagationContext)); + _syncTraceToNative(OnTraceReset(traceId, spanId)); } @override @@ -33,7 +36,6 @@ class NativeTraceSyncIntegration implements Integration { } void _syncTraceToNative(OnTraceReset event) { - final ctx = event.propagationContext; - _native.setTrace(ctx.traceId, sampleRand: ctx.sampleRand); + _native.setTrace(event.traceId, event.spanId); } } diff --git a/packages/flutter/lib/src/native/c/sentry_native.dart b/packages/flutter/lib/src/native/c/sentry_native.dart index 1f5dd4b1ae..1d082cd6d9 100644 --- a/packages/flutter/lib/src/native/c/sentry_native.dart +++ b/packages/flutter/lib/src/native/c/sentry_native.dart @@ -315,8 +315,9 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { bool get supportsTraceSync => false; @override - FutureOr setTrace(SentryId traceId, - {SpanId? spanId, double? sampleRate, double? sampleRand}) {} + FutureOr setTrace(SentryId traceId, SpanId spanId) { + _logNotSupported('setting trace'); + } } extension 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 045915aa69..2786fdf9d5 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -393,17 +393,12 @@ class SentryNativeJava extends SentryNativeChannel { bool get supportsTraceSync => true; @override - void setTrace(SentryId traceId, - {SpanId? spanId, double? sampleRate, double? sampleRand}) { + void setTrace(SentryId traceId, SpanId spanId) { tryCatchSync('setTrace', () { using((arena) { final jTraceId = traceId.toString().toJString()..releasedBy(arena); - final jSpanId = (spanId ?? SpanId.newId()).toString().toJString() - ..releasedBy(arena); - final jSampleRate = sampleRate?.toJDouble()?..releasedBy(arena); - final jSampleRand = sampleRand?.toJDouble()?..releasedBy(arena); - native.InternalSentrySdk.setTrace( - jTraceId, jSpanId, jSampleRate, jSampleRand); + final jSpanId = spanId.toString().toJString()..releasedBy(arena); + native.InternalSentrySdk.setTrace(jTraceId, jSpanId, null, null); }); }); } diff --git a/packages/flutter/lib/src/native/sentry_native_binding.dart b/packages/flutter/lib/src/native/sentry_native_binding.dart index 5a8eeeaa0c..3c5918d00c 100644 --- a/packages/flutter/lib/src/native/sentry_native_binding.dart +++ b/packages/flutter/lib/src/native/sentry_native_binding.dart @@ -98,6 +98,5 @@ abstract class SentryNativeBinding { bool get supportsTraceSync; /// Sets the trace context on the native SDK scope. - FutureOr setTrace(SentryId traceId, - {SpanId? spanId, double? sampleRate, double? sampleRand}); + FutureOr setTrace(SentryId traceId, SpanId spanId); } diff --git a/packages/flutter/lib/src/native/sentry_native_channel.dart b/packages/flutter/lib/src/native/sentry_native_channel.dart index 5515edee58..8d525597f9 100644 --- a/packages/flutter/lib/src/native/sentry_native_channel.dart +++ b/packages/flutter/lib/src/native/sentry_native_channel.dart @@ -428,8 +428,7 @@ class SentryNativeChannel bool get supportsTraceSync => !options.platform.isAndroid; @override - FutureOr setTrace(SentryId traceId, - {SpanId? spanId, double? sampleRate, double? sampleRand}) { + FutureOr setTrace(SentryId traceId, SpanId spanId) { if (options.platform.isAndroid) { assert(false, 'setTrace should not be used through method channels on Android.'); @@ -437,9 +436,7 @@ class SentryNativeChannel } return channel.invokeMethod('setTrace', { 'traceId': traceId.toString(), - if (spanId != null) 'spanId': spanId.toString(), - if (sampleRate != null) 'sampleRate': sampleRate, - if (sampleRand != null) 'sampleRand': sampleRand, + 'spanId': spanId.toString(), }); } } diff --git a/packages/flutter/lib/src/web/sentry_web.dart b/packages/flutter/lib/src/web/sentry_web.dart index 52afef5ec3..b55a12c99b 100644 --- a/packages/flutter/lib/src/web/sentry_web.dart +++ b/packages/flutter/lib/src/web/sentry_web.dart @@ -257,6 +257,11 @@ class SentryWeb with SentryNativeSafeInvoker implements SentryNativeBinding { _logNotSupported('set user'); } + @override + FutureOr setTrace(SentryId traceId, SpanId spanId) { + _logNotSupported('setting trace'); + } + @override int? startProfiler(SentryId traceId) { _logNotSupported('start profiler'); @@ -278,10 +283,6 @@ class SentryWeb with SentryNativeSafeInvoker implements SentryNativeBinding { @override bool get supportsTraceSync => false; - @override - FutureOr setTrace(SentryId traceId, - {SpanId? spanId, double? sampleRate, double? sampleRand}) {} - @override SentryFlutterOptions get options => _options; } diff --git a/packages/flutter/test/integrations/native_trace_sync_integration_test.dart b/packages/flutter/test/integrations/native_trace_sync_integration_test.dart index cdf78496be..96420ec3f8 100644 --- a/packages/flutter/test/integrations/native_trace_sync_integration_test.dart +++ b/packages/flutter/test/integrations/native_trace_sync_integration_test.dart @@ -30,7 +30,6 @@ void main() { final call = fixture.binding.setTraceCalls.single; expect(call.traceId, fixture.hub.scope.propagationContext.traceId); - expect(call.sampleRand, isNull); }); test('syncs trace when OnTraceReset is dispatched', () { @@ -45,16 +44,6 @@ void main() { expect(call.traceId, fixture.hub.scope.propagationContext.traceId); }); - test('passes sampleRand to native when set', () { - fixture.hub.scope.propagationContext.sampleRand = 0.42; - - fixture.registerIntegration(); - - final call = fixture.binding.setTraceCalls.single; - expect(call.traceId, fixture.hub.scope.propagationContext.traceId); - expect(call.sampleRand, 0.42); - }); - test('unregisters callback on close', () { fixture.registerIntegration(); fixture.binding.setTraceCalls.clear(); @@ -80,20 +69,16 @@ class Fixture { class _SetTraceCall { final SentryId traceId; - final SpanId? spanId; - final double? sampleRate; - final double? sampleRand; + final SpanId spanId; - _SetTraceCall(this.traceId, {this.spanId, this.sampleRate, this.sampleRand}); + _SetTraceCall(this.traceId, this.spanId); } class _FakeNativeBinding extends Fake implements SentryNativeBinding { final setTraceCalls = <_SetTraceCall>[]; @override - void setTrace(SentryId traceId, - {SpanId? spanId, double? sampleRate, double? sampleRand}) { - setTraceCalls.add(_SetTraceCall(traceId, - spanId: spanId, sampleRate: sampleRate, sampleRand: sampleRand)); + void setTrace(SentryId traceId, SpanId spanId) { + setTraceCalls.add(_SetTraceCall(traceId, spanId)); } } diff --git a/packages/flutter/test/mocks.mocks.dart b/packages/flutter/test/mocks.mocks.dart index 19d42d5f94..33c864a792 100644 --- a/packages/flutter/test/mocks.mocks.dart +++ b/packages/flutter/test/mocks.mocks.dart @@ -2071,19 +2071,15 @@ class MockSentryNativeBinding extends _i1.Mock @override _i12.FutureOr setTrace( - _i2.SentryId? traceId, { + _i2.SentryId? traceId, _i2.SpanId? spanId, - double? sampleRate, - double? sampleRand, - }) => + ) => (super.noSuchMethod(Invocation.method( #setTrace, - [traceId], - { - #spanId: spanId, - #sampleRate: sampleRate, - #sampleRand: sampleRand, - }, + [ + traceId, + spanId, + ], )) as _i12.FutureOr); } From 5844f3a980eb9167245a68291ee322cc43877eb7 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 12 Feb 2026 15:30:03 +0100 Subject: [PATCH 06/18] Update --- .../dart/lib/src/sdk_lifecycle_hooks.dart | 1 - packages/dart/test/hub_test.dart | 58 ++++++++++++++++--- 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/packages/dart/lib/src/sdk_lifecycle_hooks.dart b/packages/dart/lib/src/sdk_lifecycle_hooks.dart index 1c82c2e7cd..a308235f8c 100644 --- a/packages/dart/lib/src/sdk_lifecycle_hooks.dart +++ b/packages/dart/lib/src/sdk_lifecycle_hooks.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:meta/meta.dart'; import '../sentry.dart'; -import 'propagation_context.dart'; @internal typedef SdkLifecycleCallback = FutureOr diff --git a/packages/dart/test/hub_test.dart b/packages/dart/test/hub_test.dart index 1627f3faa4..e9bafae721 100644 --- a/packages/dart/test/hub_test.dart +++ b/packages/dart/test/hub_test.dart @@ -638,18 +638,62 @@ void main() { expect(newSampleRand, isNull); }); - test('generateNewTrace dispatches OnTraceReset with propagation context', + test('generateNewTrace dispatches OnTraceReset with traceId', () { + SentryId? receivedTraceId; + hub.options.lifecycleRegistry.registerCallback((event) { + receivedTraceId = event.traceId; + }); + + hub.generateNewTrace(); + + expect(receivedTraceId, isNotNull); + expect(receivedTraceId, hub.scope.propagationContext.traceId); + }); + + test( + 'generateNewTrace dispatches OnTraceReset with spanId from active span', () { - PropagationContext? receivedContext; - hub.options.lifecycleRegistry - .registerCallback((event) { - receivedContext = event.propagationContext; + SpanId? receivedSpanId; + hub.options.tracesSampleRate = 1.0; + hub.options.lifecycleRegistry.registerCallback((event) { + receivedSpanId = event.spanId; }); + hub.startTransaction('name', 'op', bindToScope: true); hub.generateNewTrace(); - expect(receivedContext, isNotNull); - expect(receivedContext, hub.scope.propagationContext); + expect(receivedSpanId, isNotNull); + expect(receivedSpanId, hub.getSpan()?.context.spanId); + }); + + test( + 'generateNewTrace dispatches OnTraceReset with new spanId if no active span', + () { + SpanId? receivedSpanId; + hub.options.tracesSampleRate = 1.0; + hub.options.lifecycleRegistry.registerCallback((event) { + receivedSpanId = event.spanId; + }); + + hub.generateNewTrace(); + + expect(hub.getSpan(), isNull); + expect(receivedSpanId, isNotNull); + }); + + test( + 'generateNewTrace dispatches OnTraceReset with new spanId if tracing is disabled', + () { + SpanId? receivedSpanId; + hub.options.tracesSampleRate = null; + hub.options.lifecycleRegistry.registerCallback((event) { + receivedSpanId = event.spanId; + }); + + hub.generateNewTrace(); + + expect(hub.getSpan(), isNull); + expect(receivedSpanId, isNotNull); }); }); From 6145db660e6ddfe25bed6b7787adf54e05a21729 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 12 Feb 2026 15:38:35 +0100 Subject: [PATCH 07/18] Update --- packages/dart/lib/src/sentry_options.dart | 7 +++++++ .../native_trace_sync_integration.dart | 6 ++++++ .../native_trace_sync_integration_test.dart | 17 +++++++++++++++++ 3 files changed, 30 insertions(+) diff --git a/packages/dart/lib/src/sentry_options.dart b/packages/dart/lib/src/sentry_options.dart index 6a40127734..2ea08939b7 100644 --- a/packages/dart/lib/src/sentry_options.dart +++ b/packages/dart/lib/src/sentry_options.dart @@ -397,6 +397,13 @@ class SentryOptions { /// Send statistics to sentry when the client drops events. bool sendClientReports = true; + /// Whether to synchronize the Dart trace to the native SDK. + /// + /// Allows native events to share the same trace as Dart events. + /// + /// Supported on Android and iOS/macOS. + bool enableNativeTraceSync = true; + /// If enabled, [scopeObservers] will be called when mutating scope. bool enableScopeSync = true; diff --git a/packages/flutter/lib/src/integrations/native_trace_sync_integration.dart b/packages/flutter/lib/src/integrations/native_trace_sync_integration.dart index c7b56c7e70..527393335c 100644 --- a/packages/flutter/lib/src/integrations/native_trace_sync_integration.dart +++ b/packages/flutter/lib/src/integrations/native_trace_sync_integration.dart @@ -4,6 +4,7 @@ import 'package:meta/meta.dart'; import '../../sentry_flutter.dart'; import '../native/sentry_native_binding.dart'; +import '../utils/internal_logger.dart'; /// Synchronizes the Dart [PropagationContext] to the native SDK so that /// native crashes/errors share the same trace as Dart events. @@ -17,6 +18,11 @@ class NativeTraceSyncIntegration implements Integration { @override void call(Hub hub, SentryFlutterOptions options) { + if (!options.enableNativeTraceSync) { + internalLogger.info('$integrationName: disabled, skipping setup'); + return; + } + _options = options; options.lifecycleRegistry .registerCallback(_syncTraceToNative); diff --git a/packages/flutter/test/integrations/native_trace_sync_integration_test.dart b/packages/flutter/test/integrations/native_trace_sync_integration_test.dart index 96420ec3f8..d1f1c1d6c1 100644 --- a/packages/flutter/test/integrations/native_trace_sync_integration_test.dart +++ b/packages/flutter/test/integrations/native_trace_sync_integration_test.dart @@ -25,6 +25,23 @@ void main() { contains(NativeTraceSyncIntegration.integrationName)); }); + test('does not add integration if enableNativeTraceSync is false', () { + fixture.options.enableNativeTraceSync = false; + fixture.registerIntegration(); + + expect(fixture.options.sdk.integrations, + isNot(contains(NativeTraceSyncIntegration.integrationName))); + }); + + test('does not sync trace if enableNativeTraceSync is false', () { + fixture.options.enableNativeTraceSync = false; + fixture.registerIntegration(); + + fixture.hub.generateNewTrace(); + + expect(fixture.binding.setTraceCalls, isEmpty); + }); + test('syncs initial propagation context on registration', () { fixture.registerIntegration(); From 2f718fc377218025fce9d668bbfbe6c6de570ef7 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 12 Feb 2026 15:41:42 +0100 Subject: [PATCH 08/18] Fix mocks --- packages/flutter/test/mocks.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/flutter/test/mocks.dart b/packages/flutter/test/mocks.dart index 7c59da2b7f..5081e79508 100644 --- a/packages/flutter/test/mocks.dart +++ b/packages/flutter/test/mocks.dart @@ -184,6 +184,7 @@ class NativeChannelFixture { handler = MockCallbacks().methodCallHandler; when(handler('initNativeSdk', any)).thenAnswer((_) => Future.value()); when(handler('closeNativeSdk', any)).thenAnswer((_) => Future.value()); + when(handler('setTrace', any)).thenAnswer((_) => Future.value()); _messenger.setMockMethodCallHandler( channel, (call) => handler(call.method, call.arguments)); } From 57a11f4cb071d035e88c5a9d02fdb7655de755ad Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 12 Feb 2026 15:45:02 +0100 Subject: [PATCH 09/18] Update --- .../flutter/example/integration_test/integration_test.dart | 1 + .../lib/src/native/java/sentry_native_java_init.dart | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/flutter/example/integration_test/integration_test.dart b/packages/flutter/example/integration_test/integration_test.dart index 20e8334d7c..2d0f4555e5 100644 --- a/packages/flutter/example/integration_test/integration_test.dart +++ b/packages/flutter/example/integration_test/integration_test.dart @@ -279,6 +279,7 @@ void main() { (p) => p.name == package.name && p.version == package.version); expect(findMatchingPackage, isNotNull); } + expect(androidOptions.isEnableAutoTraceIdGeneration(), isFalse); final androidProxy = androidOptions.getProxy(); expect(androidProxy, isNotNull); expect(androidProxy!.getHost()?.toDartString(), 'proxy.local'); diff --git a/packages/flutter/lib/src/native/java/sentry_native_java_init.dart b/packages/flutter/lib/src/native/java/sentry_native_java_init.dart index 479a98098b..a68fb2cec5 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java_init.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java_init.dart @@ -239,7 +239,11 @@ void configureAndroidOptions({ } androidOptions.setSendDefaultPii(options.sendDefaultPii); androidOptions.setEnableScopeSync(options.enableNdkScopeSync); - androidOptions.setEnableAutoTraceIdGeneration(false); + // Disable native auto trace ID generation if trace sync is enabled + // If enabled, Dart controls setting the trace ID on the native SDK. + if (options.enableNativeTraceSync) { + androidOptions.setEnableAutoTraceIdGeneration(false); + } androidOptions .setProguardUuid(options.proguardUuid?.toJString()?..releasedBy(arena)); androidOptions.setEnableSpotlight(options.spotlight.enabled); From 0181ca97173e511a5f3d3810290e9bdaf614f5b1 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 12 Feb 2026 15:45:26 +0100 Subject: [PATCH 10/18] Update doc --- packages/flutter/lib/src/native/sentry_native_binding.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter/lib/src/native/sentry_native_binding.dart b/packages/flutter/lib/src/native/sentry_native_binding.dart index 3c5918d00c..1efd177fe1 100644 --- a/packages/flutter/lib/src/native/sentry_native_binding.dart +++ b/packages/flutter/lib/src/native/sentry_native_binding.dart @@ -94,7 +94,7 @@ abstract class SentryNativeBinding { /// NNote: This is used on web platforms and is a no-op on non-web. FutureOr captureSession(); - /// Whether the native SDK supports syncing the trace id. + /// Whether the native SDK supports syncing the trace. bool get supportsTraceSync; /// Sets the trace context on the native SDK scope. From ffda3f3118b96447cb67b008b8a99df64fdaab0e Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 12 Feb 2026 15:47:27 +0100 Subject: [PATCH 11/18] Fix changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 866281eed9..4caf809621 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ - Bump Native SDK from v0.12.3 to v0.12.5 ([#3481](https://github.com/getsentry/sentry-dart/pull/3481)) - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0125) - - [diff](hgttps://github.com/getsentry/sentry-native/compare/0.12.3...0.12.5) + - [diff](https://github.com/getsentry/sentry-native/compare/0.12.3...0.12.5) - Bump Android SDK from v8.30.0 to v8.31.0 ([#3476](https://github.com/getsentry/sentry-dart/pull/3476)) - [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#8310) - [diff](https://github.com/getsentry/sentry-java/compare/8.30.0...8.31.0) From cd57dcbfdc931a30bf60ffe7a1f45e6a35b8df3c Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 12 Feb 2026 15:54:41 +0100 Subject: [PATCH 12/18] Update --- .../lib/src/integrations/native_trace_sync_integration.dart | 2 -- packages/flutter/lib/src/native/sentry_native_channel.dart | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/flutter/lib/src/integrations/native_trace_sync_integration.dart b/packages/flutter/lib/src/integrations/native_trace_sync_integration.dart index 527393335c..aac470f6fa 100644 --- a/packages/flutter/lib/src/integrations/native_trace_sync_integration.dart +++ b/packages/flutter/lib/src/integrations/native_trace_sync_integration.dart @@ -6,8 +6,6 @@ import '../../sentry_flutter.dart'; import '../native/sentry_native_binding.dart'; import '../utils/internal_logger.dart'; -/// Synchronizes the Dart [PropagationContext] to the native SDK so that -/// native crashes/errors share the same trace as Dart events. @internal class NativeTraceSyncIntegration implements Integration { static const integrationName = 'NativeTraceSync'; diff --git a/packages/flutter/lib/src/native/sentry_native_channel.dart b/packages/flutter/lib/src/native/sentry_native_channel.dart index 8d525597f9..baed785529 100644 --- a/packages/flutter/lib/src/native/sentry_native_channel.dart +++ b/packages/flutter/lib/src/native/sentry_native_channel.dart @@ -423,7 +423,7 @@ class SentryNativeChannel _logNotSupported('updating session'); } - // Android handles supportings trace sync via JNI, not method channels. + // Android handles supporting trace sync via JNI, not method channels. @override bool get supportsTraceSync => !options.platform.isAndroid; From b1666e6ef3953e22a2173f574334187778baae5f Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 12 Feb 2026 16:03:52 +0100 Subject: [PATCH 13/18] Review --- .../src/integrations/native_trace_sync_integration.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/flutter/lib/src/integrations/native_trace_sync_integration.dart b/packages/flutter/lib/src/integrations/native_trace_sync_integration.dart index aac470f6fa..80299e5363 100644 --- a/packages/flutter/lib/src/integrations/native_trace_sync_integration.dart +++ b/packages/flutter/lib/src/integrations/native_trace_sync_integration.dart @@ -1,5 +1,7 @@ // ignore_for_file: invalid_use_of_internal_member +import 'dart:async'; + import 'package:meta/meta.dart'; import '../../sentry_flutter.dart'; @@ -39,7 +41,6 @@ class NativeTraceSyncIntegration implements Integration { .removeCallback(_syncTraceToNative); } - void _syncTraceToNative(OnTraceReset event) { - _native.setTrace(event.traceId, event.spanId); - } + FutureOr _syncTraceToNative(OnTraceReset event) => + _native.setTrace(event.traceId, event.spanId); } From 1a99f498a966b7b69aec3e89c126dd67812645db Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 12 Feb 2026 16:44:43 +0100 Subject: [PATCH 14/18] Update --- packages/dart/lib/src/hub.dart | 4 ++-- packages/dart/lib/src/propagation_context.dart | 2 +- packages/dart/lib/src/sdk_lifecycle_hooks.dart | 4 ++-- packages/dart/test/hub_test.dart | 12 ++++++++---- packages/dart/test/propagation_context_test.dart | 2 +- .../example/integration_test/integration_test.dart | 2 -- .../integrations/native_trace_sync_integration.dart | 8 ++++---- .../lib/src/native/java/sentry_native_java.dart | 6 ++++++ 8 files changed, 24 insertions(+), 16 deletions(-) diff --git a/packages/dart/lib/src/hub.dart b/packages/dart/lib/src/hub.dart index bf512874e3..77a1c8581e 100644 --- a/packages/dart/lib/src/hub.dart +++ b/packages/dart/lib/src/hub.dart @@ -609,10 +609,10 @@ class Hub { void generateNewTrace() { // Create a brand-new trace and reset the sampling flag and sampleRand so // that the next root transaction can set it again. - scope.propagationContext.resetTrace(); + scope.propagationContext.generateNewTrace(); // Fire-and-forget the callback // Native SDK synchronization over async method channels may be slightly delayed, but this is not problematic in practice. - _options.lifecycleRegistry.dispatchCallback(OnTraceReset( + _options.lifecycleRegistry.dispatchCallback(OnGenerateNewTrace( scope.propagationContext.traceId, getSpan()?.context.spanId ?? SpanId.newId())); } diff --git a/packages/dart/lib/src/propagation_context.dart b/packages/dart/lib/src/propagation_context.dart index b75a897383..167bf761be 100644 --- a/packages/dart/lib/src/propagation_context.dart +++ b/packages/dart/lib/src/propagation_context.dart @@ -39,7 +39,7 @@ class PropagationContext { double? sampleRand; /// Starts a brand-new trace (new ID, new sampling value & sampled state). - void resetTrace() { + void generateNewTrace() { traceId = SentryId.newId(); sampleRand = null; _sampled = null; diff --git a/packages/dart/lib/src/sdk_lifecycle_hooks.dart b/packages/dart/lib/src/sdk_lifecycle_hooks.dart index a308235f8c..219e8ff49d 100644 --- a/packages/dart/lib/src/sdk_lifecycle_hooks.dart +++ b/packages/dart/lib/src/sdk_lifecycle_hooks.dart @@ -106,9 +106,9 @@ class OnProcessMetric extends SdkLifecycleEvent { /// Dispatched when a new trace is generated via [Hub.generateNewTrace]. @internal -class OnTraceReset extends SdkLifecycleEvent { +class OnGenerateNewTrace extends SdkLifecycleEvent { final SentryId traceId; final SpanId spanId; - OnTraceReset(this.traceId, this.spanId); + OnGenerateNewTrace(this.traceId, this.spanId); } diff --git a/packages/dart/test/hub_test.dart b/packages/dart/test/hub_test.dart index e9bafae721..475e057497 100644 --- a/packages/dart/test/hub_test.dart +++ b/packages/dart/test/hub_test.dart @@ -640,7 +640,8 @@ void main() { test('generateNewTrace dispatches OnTraceReset with traceId', () { SentryId? receivedTraceId; - hub.options.lifecycleRegistry.registerCallback((event) { + hub.options.lifecycleRegistry + .registerCallback((event) { receivedTraceId = event.traceId; }); @@ -655,7 +656,8 @@ void main() { () { SpanId? receivedSpanId; hub.options.tracesSampleRate = 1.0; - hub.options.lifecycleRegistry.registerCallback((event) { + hub.options.lifecycleRegistry + .registerCallback((event) { receivedSpanId = event.spanId; }); @@ -671,7 +673,8 @@ void main() { () { SpanId? receivedSpanId; hub.options.tracesSampleRate = 1.0; - hub.options.lifecycleRegistry.registerCallback((event) { + hub.options.lifecycleRegistry + .registerCallback((event) { receivedSpanId = event.spanId; }); @@ -686,7 +689,8 @@ void main() { () { SpanId? receivedSpanId; hub.options.tracesSampleRate = null; - hub.options.lifecycleRegistry.registerCallback((event) { + hub.options.lifecycleRegistry + .registerCallback((event) { receivedSpanId = event.spanId; }); diff --git a/packages/dart/test/propagation_context_test.dart b/packages/dart/test/propagation_context_test.dart index 22347faa3a..7f128728ab 100644 --- a/packages/dart/test/propagation_context_test.dart +++ b/packages/dart/test/propagation_context_test.dart @@ -126,7 +126,7 @@ void main() { sut.sampleRand = 1.0; sut.applySamplingDecision(true); - sut.resetTrace(); + sut.generateNewTrace(); expect(sut.traceId, isNot(traceId)); expect(sut.sampleRand, isNull); diff --git a/packages/flutter/example/integration_test/integration_test.dart b/packages/flutter/example/integration_test/integration_test.dart index 2d0f4555e5..787f47a80a 100644 --- a/packages/flutter/example/integration_test/integration_test.dart +++ b/packages/flutter/example/integration_test/integration_test.dart @@ -1089,8 +1089,6 @@ void main() { Sentry.currentHub.generateNewTrace(); final newDartTraceId = Sentry.currentHub.scope.propagationContext.traceId.toString(); - expect(newDartTraceId, isNot(dartTraceId), - reason: 'New trace should have a different traceId'); // Allow the fire-and-forget dispatch to complete await Future.delayed(const Duration(milliseconds: 100)); diff --git a/packages/flutter/lib/src/integrations/native_trace_sync_integration.dart b/packages/flutter/lib/src/integrations/native_trace_sync_integration.dart index 80299e5363..53308cb348 100644 --- a/packages/flutter/lib/src/integrations/native_trace_sync_integration.dart +++ b/packages/flutter/lib/src/integrations/native_trace_sync_integration.dart @@ -25,22 +25,22 @@ class NativeTraceSyncIntegration implements Integration { _options = options; options.lifecycleRegistry - .registerCallback(_syncTraceToNative); + .registerCallback(_syncTraceToNative); options.sdk.addIntegration(integrationName); final traceId = hub.scope.propagationContext.traceId; final spanId = hub.getSpan()?.context.spanId ?? SpanId.newId(); // Sync the initial PropagationContext created at Hub construction. - _syncTraceToNative(OnTraceReset(traceId, spanId)); + _syncTraceToNative(OnGenerateNewTrace(traceId, spanId)); } @override void close() { _options?.lifecycleRegistry - .removeCallback(_syncTraceToNative); + .removeCallback(_syncTraceToNative); } - FutureOr _syncTraceToNative(OnTraceReset event) => + FutureOr _syncTraceToNative(OnGenerateNewTrace event) => _native.setTrace(event.traceId, event.spanId); } 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 2786fdf9d5..a57ea31a47 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -398,6 +398,12 @@ class SentryNativeJava extends SentryNativeChannel { using((arena) { final jTraceId = traceId.toString().toJString()..releasedBy(arena); final jSpanId = spanId.toString().toJString()..releasedBy(arena); + // The two double parameters are sampleRate and sampleRand. + // We pass null for them because we don't need to support sampleRate and sampleRand. + // sampleRate and sampleRand are only used by native for baggage headers + // on outgoing HTTP requests. Since HTTP requests in Flutter go through + // Dart, the Dart-side propagation context handles baggage already. + // When there is a use case for sampleRate and sampleRand, we can add support for them. native.InternalSentrySdk.setTrace(jTraceId, jSpanId, null, null); }); }); From 6024593fd253b6c4a66cda4faf1f04c272e9fa69 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 12 Feb 2026 16:46:02 +0100 Subject: [PATCH 15/18] Bubble up error from native flutter plugin in swift --- .../Sources/sentry_flutter/SentryFlutterPlugin.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift index 7e3116a4f4..8281b488b5 100644 --- a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift +++ b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift @@ -704,6 +704,8 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { private func setTrace(traceId: String?, spanId: String?, result: @escaping FlutterResult) { guard let traceId = traceId, let spanId = spanId else { + print("Cannot set trace: traceId or spanId is null") + result(FlutterError(code: "10", message: "Cannot set trace: traceId or spanId is null", details: nil)) result("") return } From f65be25dff2c8a008856c435b428bc35aef92727 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 12 Feb 2026 16:49:44 +0100 Subject: [PATCH 16/18] Move enableNativeTraceSync from dart to flutter options --- packages/dart/lib/src/sentry_options.dart | 7 ------- packages/flutter/lib/src/sentry_flutter_options.dart | 7 +++++++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/dart/lib/src/sentry_options.dart b/packages/dart/lib/src/sentry_options.dart index 2ea08939b7..6a40127734 100644 --- a/packages/dart/lib/src/sentry_options.dart +++ b/packages/dart/lib/src/sentry_options.dart @@ -397,13 +397,6 @@ class SentryOptions { /// Send statistics to sentry when the client drops events. bool sendClientReports = true; - /// Whether to synchronize the Dart trace to the native SDK. - /// - /// Allows native events to share the same trace as Dart events. - /// - /// Supported on Android and iOS/macOS. - bool enableNativeTraceSync = true; - /// If enabled, [scopeObservers] will be called when mutating scope. bool enableScopeSync = true; diff --git a/packages/flutter/lib/src/sentry_flutter_options.dart b/packages/flutter/lib/src/sentry_flutter_options.dart index 0de7aebfe6..d88afa0209 100644 --- a/packages/flutter/lib/src/sentry_flutter_options.dart +++ b/packages/flutter/lib/src/sentry_flutter_options.dart @@ -284,6 +284,13 @@ class SentryFlutterOptions extends SentryOptions { /// you must use `SentryWidgetsFlutterBinding.ensureInitialized()` instead. bool enableFramesTracking = true; + /// Whether to synchronize the Dart trace to the native SDK. + /// + /// Allows native events to share the same trace as Dart events. + /// + /// Supported on Android and iOS/macOS. + bool enableNativeTraceSync = true; + /// Replay recording configuration. final replay = SentryReplayOptions(); From 18234b870b5e0bf095dd671237c76770f76a2df6 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 12 Feb 2026 16:50:26 +0100 Subject: [PATCH 17/18] Fix duplicate result --- .../Sources/sentry_flutter/SentryFlutterPlugin.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift index 8281b488b5..a894c8daf5 100644 --- a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift +++ b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift @@ -706,7 +706,6 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { guard let traceId = traceId, let spanId = spanId else { print("Cannot set trace: traceId or spanId is null") result(FlutterError(code: "10", message: "Cannot set trace: traceId or spanId is null", details: nil)) - result("") return } let sentryTraceId = SentryId(uuidString: traceId) From 206ef8a2a2255462e775054e2142b73cca21beea Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 12 Feb 2026 16:56:13 +0100 Subject: [PATCH 18/18] Improve documentation --- .../flutter/lib/src/native/java/sentry_native_java_init.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/flutter/lib/src/native/java/sentry_native_java_init.dart b/packages/flutter/lib/src/native/java/sentry_native_java_init.dart index a68fb2cec5..86004d5bc3 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java_init.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java_init.dart @@ -239,8 +239,9 @@ void configureAndroidOptions({ } androidOptions.setSendDefaultPii(options.sendDefaultPii); androidOptions.setEnableScopeSync(options.enableNdkScopeSync); - // Disable native auto trace ID generation if trace sync is enabled - // If enabled, Dart controls setting the trace ID on the native SDK. + // When trace sync is enabled, Dart is the source of truth for propagation + // context and pushes it to native via setTrace. Disable native auto + // generation so it doesn't overwrite the Dart-provided trace ID. if (options.enableNativeTraceSync) { androidOptions.setEnableAutoTraceIdGeneration(false); }