From 7e891ab483c7b1dc9bf7ff28e4da6e043a1dc9a6 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 17 Mar 2026 14:18:04 +0100 Subject: [PATCH 1/8] feat: Implement strict trace continuation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Parse org_id from DSN host (e.g. `o123.ingest.sentry.io` → `123`) - Add `strictTraceContinuation` and `orgId` options to SentryOptions - Propagate `sentry-org_id` in baggage/DSC - Validate incoming traces: mismatched org IDs start a new trace; strict mode also rejects traces with missing org IDs Spec: https://develop.sentry.dev/sdk/foundations/trace-propagation/#strict-trace-continuation Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 7 + packages/dart/lib/src/protocol/dsn.dart | 12 + packages/dart/lib/src/sentry_baggage.dart | 10 + packages/dart/lib/src/sentry_options.dart | 33 ++ .../lib/src/sentry_trace_context_header.dart | 10 + packages/dart/lib/src/sentry_tracer.dart | 1 + .../lib/src/sentry_transaction_context.dart | 16 + .../dart/lib/src/utils/tracing_utils.dart | 43 +++ .../test/strict_trace_continuation_test.dart | 356 ++++++++++++++++++ 9 files changed, 488 insertions(+) create mode 100644 packages/dart/test/strict_trace_continuation_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 97f8de6df9..317e393002 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## Unreleased +### Features + +- Prevent cross-organization trace continuation ([#XXXX](https://github.com/getsentry/sentry-dart/pull/XXXX)) + - By default, the SDK now extracts the organization ID from the DSN (e.g. `o123.ingest.sentry.io`) and compares it with the `sentry-org_id` value in incoming baggage headers. When the two differ, the SDK starts a fresh trace instead of continuing the foreign one. This guards against accidentally linking traces across organizations. + - New option `strictTraceContinuation` (default `false`): when enabled, both the SDK's org ID **and** the incoming baggage org ID must be present and match for a trace to be continued. Traces with a missing org ID on either side are rejected. + - New option `orgId`: allows explicitly setting the organization ID for self-hosted and Relay setups where it cannot be extracted from the DSN. + ### Fixes - Stop re-triggering hitTest in SentryUserInteractionWidget on pointerUp ([#3540](https://github.com/getsentry/sentry-dart/pull/3540)) diff --git a/packages/dart/lib/src/protocol/dsn.dart b/packages/dart/lib/src/protocol/dsn.dart index c3ec5093c8..36641c8709 100644 --- a/packages/dart/lib/src/protocol/dsn.dart +++ b/packages/dart/lib/src/protocol/dsn.dart @@ -1,5 +1,17 @@ import 'package:meta/meta.dart'; +/// Regex to extract the org ID from a DSN host (e.g. `o123.ingest.sentry.io` -> `123`). +final RegExp _orgIdFromHostRegExp = RegExp(r'^o(\d+)\.'); + +/// Extracts the organization ID from a DSN host string. +/// +/// Returns the numeric org ID as a string, or `null` if the host does not +/// match the expected pattern (e.g. `o123.ingest.sentry.io`). +String? extractOrgIdFromDsnHost(String host) { + final match = _orgIdFromHostRegExp.firstMatch(host); + return match?.group(1); +} + /// The Data Source Name (DSN) tells the SDK where to send the events @immutable class Dsn { diff --git a/packages/dart/lib/src/sentry_baggage.dart b/packages/dart/lib/src/sentry_baggage.dart index 37232aa4d9..42f8c6fe1c 100644 --- a/packages/dart/lib/src/sentry_baggage.dart +++ b/packages/dart/lib/src/sentry_baggage.dart @@ -97,6 +97,10 @@ class SentryBaggage { if (scope.replayId != null && scope.replayId != SentryId.empty()) { setReplayId(scope.replayId.toString()); } + final effectiveOrgId = options.effectiveOrgId; + if (effectiveOrgId != null) { + setOrgId(effectiveOrgId); + } } static Map _extractKeyValuesFromBaggageString( @@ -195,6 +199,12 @@ class SentryBaggage { return double.tryParse(sampleRand); } + void setOrgId(String value) { + set('sentry-org_id', value); + } + + String? getOrgId() => get('sentry-org_id'); + void setReplayId(String value) => set('sentry-replay_id', value); SentryId? getReplayId() { diff --git a/packages/dart/lib/src/sentry_options.dart b/packages/dart/lib/src/sentry_options.dart index 6a40127734..6713857092 100644 --- a/packages/dart/lib/src/sentry_options.dart +++ b/packages/dart/lib/src/sentry_options.dart @@ -568,6 +568,39 @@ class SentryOptions { /// Enabling this option may change grouping. bool includeModuleInStackTrace = false; + /// Whether the SDK requires matching org IDs to continue an incoming trace. + /// + /// When `true`, both the SDK's org ID and the incoming baggage `sentry-org_id` + /// must be present and match for a trace to be continued. When `false` + /// (the default), a mismatch between present org IDs still starts a new + /// trace, but missing org IDs on either side are tolerated. + bool strictTraceContinuation = false; + + /// The organization ID for your Sentry project. + /// + /// The SDK tries to extract the organization ID from the DSN automatically. + /// If it cannot be found, or if you need to override it, provide the ID + /// with this option. The organization ID is used for trace propagation and + /// for features like [strictTraceContinuation]. + String? orgId; + + /// The effective organization ID, preferring [orgId] over the DSN-parsed value. + @internal + String? get effectiveOrgId { + if (orgId != null) { + return orgId; + } + try { + final host = parsedDsn.uri?.host; + if (host != null) { + return extractOrgIdFromDsnHost(host); + } + } catch (_) { + // DSN may not be set or parseable + } + return null; + } + @internal late SentryLogger logger = const NoOpSentryLogger(); diff --git a/packages/dart/lib/src/sentry_trace_context_header.dart b/packages/dart/lib/src/sentry_trace_context_header.dart index 7d5d5d79ec..a636d8283f 100644 --- a/packages/dart/lib/src/sentry_trace_context_header.dart +++ b/packages/dart/lib/src/sentry_trace_context_header.dart @@ -17,6 +17,7 @@ class SentryTraceContextHeader { this.sampled, this.unknown, this.replayId, + this.orgId, }); final SentryId traceId; @@ -35,6 +36,9 @@ class SentryTraceContextHeader { @internal SentryId? replayId; + /// The organization ID associated with this trace. + final String? orgId; + /// Deserializes a [SentryTraceContextHeader] from JSON [Map]. factory SentryTraceContextHeader.fromJson(Map data) { final json = AccessAwareMap(data); @@ -49,6 +53,7 @@ class SentryTraceContextHeader { sampled: json['sampled'], replayId: json['replay_id'] == null ? null : SentryId.fromId(json['replay_id']), + orgId: json['org_id'] as String?, unknown: json.notAccessed(), ); } @@ -66,6 +71,7 @@ class SentryTraceContextHeader { if (sampleRate != null) 'sample_rate': sampleRate, if (sampled != null) 'sampled': sampled, if (replayId != null) 'replay_id': replayId.toString(), + if (orgId != null) 'org_id': orgId, }; } @@ -98,6 +104,9 @@ class SentryTraceContextHeader { if (replayId != null) { baggage.setReplayId(replayId.toString()); } + if (orgId != null) { + baggage.setOrgId(orgId!); + } return baggage; } @@ -109,6 +118,7 @@ class SentryTraceContextHeader { release: baggage.get('sentry-release'), environment: baggage.get('sentry-environment'), replayId: baggage.getReplayId(), + orgId: baggage.getOrgId(), ); } } diff --git a/packages/dart/lib/src/sentry_tracer.dart b/packages/dart/lib/src/sentry_tracer.dart index 0b8e5468db..154ca32c07 100644 --- a/packages/dart/lib/src/sentry_tracer.dart +++ b/packages/dart/lib/src/sentry_tracer.dart @@ -391,6 +391,7 @@ class SentryTracer extends ISentrySpan { sampleRate: _sampleRateToString(_rootSpan.samplingDecision?.sampleRate), sampleRand: _sampleRandToString(_rootSpan.samplingDecision?.sampleRand), sampled: _rootSpan.samplingDecision?.sampled.toString(), + orgId: _hub.options.effectiveOrgId, ); return _sentryTraceContextHeader; diff --git a/packages/dart/lib/src/sentry_transaction_context.dart b/packages/dart/lib/src/sentry_transaction_context.dart index 5002cb9b40..b779c57199 100644 --- a/packages/dart/lib/src/sentry_transaction_context.dart +++ b/packages/dart/lib/src/sentry_transaction_context.dart @@ -1,7 +1,9 @@ import 'protocol.dart'; import 'sentry_baggage.dart'; +import 'sentry_options.dart'; import 'sentry_trace_origins.dart'; import 'tracing.dart'; +import 'utils/tracing_utils.dart'; class SentryTransactionContext extends SentrySpanContext { String name; @@ -30,7 +32,21 @@ class SentryTransactionContext extends SentrySpanContext { SentryTraceHeader traceHeader, { SentryTransactionNameSource? transactionNameSource, SentryBaggage? baggage, + SentryOptions? options, }) { + // Validate org ID before continuing the incoming trace + if (options != null && + !shouldContinueTrace(options, baggage?.getOrgId())) { + // Start a new trace instead of continuing the incoming one + return SentryTransactionContext( + name, + operation, + transactionNameSource: + transactionNameSource ?? SentryTransactionNameSource.custom, + origin: SentryTraceOrigins.manual, + ); + } + final sampleRate = baggage?.getSampleRate(); final sampleRand = baggage?.getSampleRand(); return SentryTransactionContext( diff --git a/packages/dart/lib/src/utils/tracing_utils.dart b/packages/dart/lib/src/utils/tracing_utils.dart index e89ab3a92d..0f9ed00558 100644 --- a/packages/dart/lib/src/utils/tracing_utils.dart +++ b/packages/dart/lib/src/utils/tracing_utils.dart @@ -132,6 +132,49 @@ bool containsTargetOrMatchesRegExp( return false; } +/// Determines whether an incoming trace should be continued based on org ID matching. +/// +/// Returns `true` if the trace should be continued, `false` if a new trace +/// should be started instead. +/// +/// The decision matrix: +/// - Both org IDs present and matching: continue +/// - Both org IDs present and different: new trace (always) +/// - One or both missing, strict=false: continue +/// - One or both missing, strict=true: new trace (unless both missing) +bool shouldContinueTrace(SentryOptions options, String? baggageOrgId) { + final sdkOrgId = options.effectiveOrgId; + + // Mismatched org IDs always reject regardless of strict mode + if (sdkOrgId != null && baggageOrgId != null && sdkOrgId != baggageOrgId) { + options.log( + SentryLevel.debug, + "Not continuing trace because org IDs don't match " + '(incoming baggage: $baggageOrgId, SDK: $sdkOrgId)', + ); + return false; + } + + if (options.strictTraceContinuation) { + // Both missing is OK + if (sdkOrgId == null && baggageOrgId == null) { + return true; + } + // One missing means reject + if (sdkOrgId == null || baggageOrgId == null) { + options.log( + SentryLevel.debug, + 'Starting a new trace because strict trace continuation is enabled ' + 'but one org ID is missing ' + '(incoming baggage: $baggageOrgId, SDK: $sdkOrgId)', + ); + return false; + } + } + + return true; +} + bool isValidSampleRate(double? sampleRate) { if (sampleRate == null) { return false; diff --git a/packages/dart/test/strict_trace_continuation_test.dart b/packages/dart/test/strict_trace_continuation_test.dart new file mode 100644 index 0000000000..b24fc564af --- /dev/null +++ b/packages/dart/test/strict_trace_continuation_test.dart @@ -0,0 +1,356 @@ +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/utils/tracing_utils.dart'; +import 'package:test/test.dart'; + +import 'test_utils.dart'; + +void main() { + group('extractOrgIdFromDsnHost', () { + test('extracts org id from standard host', () { + expect(extractOrgIdFromDsnHost('o123.ingest.sentry.io'), '123'); + }); + + test('extracts single digit org id', () { + expect(extractOrgIdFromDsnHost('o1.ingest.us.sentry.io'), '1'); + }); + + test('extracts large org id', () { + expect(extractOrgIdFromDsnHost('o9999999.ingest.sentry.io'), '9999999'); + }); + + test('returns null for host without org prefix', () { + expect(extractOrgIdFromDsnHost('sentry.io'), isNull); + }); + + test('returns null for localhost', () { + expect(extractOrgIdFromDsnHost('localhost'), isNull); + }); + + test('returns null for empty string', () { + expect(extractOrgIdFromDsnHost(''), isNull); + }); + + test('returns null for non-numeric org id', () { + expect(extractOrgIdFromDsnHost('oabc.ingest.sentry.io'), isNull); + }); + }); + + group('SentryOptions', () { + group('effectiveOrgId', () { + test('returns null when neither orgId nor DSN org id are set', () { + final options = defaultTestOptions(); + expect(options.effectiveOrgId, isNull); + }); + + test('returns explicit orgId when set', () { + final options = defaultTestOptions()..orgId = '456'; + expect(options.effectiveOrgId, '456'); + }); + + test('returns DSN-extracted org id when orgId is not set', () { + final options = SentryOptions( + dsn: 'https://public@o123.ingest.sentry.io/1') + ..automatedTestMode = true; + expect(options.effectiveOrgId, '123'); + }); + + test('prefers explicit orgId over DSN-extracted org id', () { + final options = SentryOptions( + dsn: 'https://public@o123.ingest.sentry.io/1') + ..automatedTestMode = true + ..orgId = '456'; + expect(options.effectiveOrgId, '456'); + }); + }); + + test('strictTraceContinuation defaults to false', () { + final options = defaultTestOptions(); + expect(options.strictTraceContinuation, isFalse); + }); + + test('orgId defaults to null', () { + final options = defaultTestOptions(); + expect(options.orgId, isNull); + }); + }); + + group('shouldContinueTrace', () { + test('returns true when both org IDs are null', () { + final options = defaultTestOptions(); + expect(shouldContinueTrace(options, null), isTrue); + }); + + test('returns true when org IDs match', () { + final options = defaultTestOptions()..orgId = '123'; + expect(shouldContinueTrace(options, '123'), isTrue); + }); + + test('returns false when org IDs do not match', () { + final options = defaultTestOptions()..orgId = '123'; + expect(shouldContinueTrace(options, '456'), isFalse); + }); + + group('when strictTraceContinuation is false', () { + test('continues trace when baggage org ID is missing', () { + final options = defaultTestOptions() + ..orgId = '123' + ..strictTraceContinuation = false; + expect(shouldContinueTrace(options, null), isTrue); + }); + + test('continues trace when SDK org ID is missing', () { + final options = defaultTestOptions() + ..strictTraceContinuation = false; + expect(shouldContinueTrace(options, '123'), isTrue); + }); + }); + + group('when strictTraceContinuation is true', () { + test('starts new trace when baggage org ID is missing', () { + final options = defaultTestOptions() + ..orgId = '123' + ..strictTraceContinuation = true; + expect(shouldContinueTrace(options, null), isFalse); + }); + + test('starts new trace when SDK org ID is missing', () { + final options = defaultTestOptions() + ..strictTraceContinuation = true; + expect(shouldContinueTrace(options, '123'), isFalse); + }); + + test('continues trace when both org IDs are missing', () { + final options = defaultTestOptions() + ..strictTraceContinuation = true; + expect(shouldContinueTrace(options, null), isTrue); + }); + + test('continues trace when org IDs match', () { + final options = defaultTestOptions() + ..orgId = '123' + ..strictTraceContinuation = true; + expect(shouldContinueTrace(options, '123'), isTrue); + }); + + test('starts new trace when org IDs do not match', () { + final options = defaultTestOptions() + ..orgId = '123' + ..strictTraceContinuation = true; + expect(shouldContinueTrace(options, '456'), isFalse); + }); + }); + }); + + group('SentryBaggage', () { + test('sets and gets org_id', () { + final baggage = SentryBaggage({}); + baggage.setOrgId('123'); + expect(baggage.getOrgId(), '123'); + }); + + test('returns null for missing org_id', () { + final baggage = SentryBaggage({}); + expect(baggage.getOrgId(), isNull); + }); + + test('setValuesFromScope includes org_id when available', () { + final options = SentryOptions( + dsn: 'https://public@o123.ingest.sentry.io/1') + ..automatedTestMode = true; + final scope = Scope(options); + final baggage = SentryBaggage({}); + + baggage.setValuesFromScope(scope, options); + expect(baggage.getOrgId(), '123'); + }); + + test('setValuesFromScope includes explicit orgId', () { + final options = defaultTestOptions()..orgId = '456'; + final scope = Scope(options); + final baggage = SentryBaggage({}); + + baggage.setValuesFromScope(scope, options); + expect(baggage.getOrgId(), '456'); + }); + + test('setValuesFromScope does not include org_id when not available', () { + final options = defaultTestOptions(); + final scope = Scope(options); + final baggage = SentryBaggage({}); + + baggage.setValuesFromScope(scope, options); + expect(baggage.getOrgId(), isNull); + }); + + test('org_id is included in header string', () { + final baggage = SentryBaggage({}); + baggage.setOrgId('123'); + expect(baggage.toHeaderString(), contains('sentry-org_id=123')); + }); + }); + + group('SentryTraceContextHeader', () { + test('includes orgId in toBaggage', () { + final context = SentryTraceContextHeader( + SentryId.newId(), + 'publicKey', + orgId: '123', + ); + final baggage = context.toBaggage(); + expect(baggage.getOrgId(), '123'); + }); + + test('does not include orgId in toBaggage when null', () { + final context = SentryTraceContextHeader( + SentryId.newId(), + 'publicKey', + ); + final baggage = context.toBaggage(); + expect(baggage.getOrgId(), isNull); + }); + + test('includes orgId in toJson', () { + final context = SentryTraceContextHeader( + SentryId.newId(), + 'publicKey', + orgId: '123', + ); + final json = context.toJson(); + expect(json['org_id'], '123'); + }); + + test('reads orgId from fromJson', () { + final context = SentryTraceContextHeader.fromJson({ + 'trace_id': SentryId.newId().toString(), + 'public_key': 'publicKey', + 'org_id': '123', + }); + expect(context.orgId, '123'); + }); + + test('reads orgId from fromBaggage', () { + final baggage = SentryBaggage({}); + baggage.setTraceId(SentryId.newId().toString()); + baggage.setPublicKey('publicKey'); + baggage.setOrgId('123'); + + final context = SentryTraceContextHeader.fromBaggage(baggage); + expect(context.orgId, '123'); + }); + }); + + group('SentryTransactionContext', () { + final traceId = SentryId.fromId('12312012123120121231201212312012'); + final spanId = SpanId.fromId('1121201211212012'); + + group('fromSentryTrace', () { + test('continues trace when org IDs match', () { + final options = defaultTestOptions()..orgId = '123'; + final header = + SentryTraceHeader(traceId, spanId, sampled: true); + final baggage = SentryBaggage({})..setOrgId('123'); + + final context = SentryTransactionContext.fromSentryTrace( + 'name', + 'op', + header, + baggage: baggage, + options: options, + ); + + expect(context.traceId, traceId); + expect(context.parentSpanId, spanId); + }); + + test('starts new trace when org IDs do not match', () { + final options = defaultTestOptions()..orgId = '123'; + final header = + SentryTraceHeader(traceId, spanId, sampled: true); + final baggage = SentryBaggage({})..setOrgId('456'); + + final context = SentryTransactionContext.fromSentryTrace( + 'name', + 'op', + header, + baggage: baggage, + options: options, + ); + + expect(context.traceId, isNot(traceId)); + expect(context.parentSpanId, isNull); + }); + + test('continues trace when options is null (backwards compat)', () { + final header = + SentryTraceHeader(traceId, spanId, sampled: true); + final baggage = SentryBaggage({})..setOrgId('456'); + + final context = SentryTransactionContext.fromSentryTrace( + 'name', + 'op', + header, + baggage: baggage, + ); + + expect(context.traceId, traceId); + expect(context.parentSpanId, spanId); + }); + + group('when strictTraceContinuation is true', () { + test('starts new trace when baggage org ID is missing', () { + final options = defaultTestOptions() + ..orgId = '123' + ..strictTraceContinuation = true; + final header = + SentryTraceHeader(traceId, spanId, sampled: true); + + final context = SentryTransactionContext.fromSentryTrace( + 'name', + 'op', + header, + options: options, + ); + + expect(context.traceId, isNot(traceId)); + expect(context.parentSpanId, isNull); + }); + + test('starts new trace when SDK org ID is missing', () { + final options = defaultTestOptions() + ..strictTraceContinuation = true; + final header = + SentryTraceHeader(traceId, spanId, sampled: true); + final baggage = SentryBaggage({})..setOrgId('123'); + + final context = SentryTransactionContext.fromSentryTrace( + 'name', + 'op', + header, + baggage: baggage, + options: options, + ); + + expect(context.traceId, isNot(traceId)); + expect(context.parentSpanId, isNull); + }); + + test('continues trace when both org IDs are missing', () { + final options = defaultTestOptions() + ..strictTraceContinuation = true; + final header = + SentryTraceHeader(traceId, spanId, sampled: true); + + final context = SentryTransactionContext.fromSentryTrace( + 'name', + 'op', + header, + options: options, + ); + + expect(context.traceId, traceId); + expect(context.parentSpanId, spanId); + }); + }); + }); + }); +} From a48e7d72978ad1c9d3ed103644f43f1d393d2157 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 17 Mar 2026 15:02:16 +0100 Subject: [PATCH 2/8] fix: Apply dart format and update changelog PR number --- CHANGELOG.md | 2 +- .../lib/src/sentry_transaction_context.dart | 3 +-- packages/dart/lib/src/utils/tracing_utils.dart | 6 +++--- .../test/strict_trace_continuation_test.dart | 18 ++++++------------ 4 files changed, 11 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 317e393002..c23076c613 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features -- Prevent cross-organization trace continuation ([#XXXX](https://github.com/getsentry/sentry-dart/pull/XXXX)) +- Prevent cross-organization trace continuation ([#3567](https://github.com/getsentry/sentry-dart/pull/3567)) - By default, the SDK now extracts the organization ID from the DSN (e.g. `o123.ingest.sentry.io`) and compares it with the `sentry-org_id` value in incoming baggage headers. When the two differ, the SDK starts a fresh trace instead of continuing the foreign one. This guards against accidentally linking traces across organizations. - New option `strictTraceContinuation` (default `false`): when enabled, both the SDK's org ID **and** the incoming baggage org ID must be present and match for a trace to be continued. Traces with a missing org ID on either side are rejected. - New option `orgId`: allows explicitly setting the organization ID for self-hosted and Relay setups where it cannot be extracted from the DSN. diff --git a/packages/dart/lib/src/sentry_transaction_context.dart b/packages/dart/lib/src/sentry_transaction_context.dart index b779c57199..601a885e6f 100644 --- a/packages/dart/lib/src/sentry_transaction_context.dart +++ b/packages/dart/lib/src/sentry_transaction_context.dart @@ -35,8 +35,7 @@ class SentryTransactionContext extends SentrySpanContext { SentryOptions? options, }) { // Validate org ID before continuing the incoming trace - if (options != null && - !shouldContinueTrace(options, baggage?.getOrgId())) { + if (options != null && !shouldContinueTrace(options, baggage?.getOrgId())) { // Start a new trace instead of continuing the incoming one return SentryTransactionContext( name, diff --git a/packages/dart/lib/src/utils/tracing_utils.dart b/packages/dart/lib/src/utils/tracing_utils.dart index 0f9ed00558..b0d5528481 100644 --- a/packages/dart/lib/src/utils/tracing_utils.dart +++ b/packages/dart/lib/src/utils/tracing_utils.dart @@ -150,7 +150,7 @@ bool shouldContinueTrace(SentryOptions options, String? baggageOrgId) { options.log( SentryLevel.debug, "Not continuing trace because org IDs don't match " - '(incoming baggage: $baggageOrgId, SDK: $sdkOrgId)', + '(incoming baggage: $baggageOrgId, SDK: $sdkOrgId)', ); return false; } @@ -165,8 +165,8 @@ bool shouldContinueTrace(SentryOptions options, String? baggageOrgId) { options.log( SentryLevel.debug, 'Starting a new trace because strict trace continuation is enabled ' - 'but one org ID is missing ' - '(incoming baggage: $baggageOrgId, SDK: $sdkOrgId)', + 'but one org ID is missing ' + '(incoming baggage: $baggageOrgId, SDK: $sdkOrgId)', ); return false; } diff --git a/packages/dart/test/strict_trace_continuation_test.dart b/packages/dart/test/strict_trace_continuation_test.dart index b24fc564af..a782b099e0 100644 --- a/packages/dart/test/strict_trace_continuation_test.dart +++ b/packages/dart/test/strict_trace_continuation_test.dart @@ -246,8 +246,7 @@ void main() { group('fromSentryTrace', () { test('continues trace when org IDs match', () { final options = defaultTestOptions()..orgId = '123'; - final header = - SentryTraceHeader(traceId, spanId, sampled: true); + final header = SentryTraceHeader(traceId, spanId, sampled: true); final baggage = SentryBaggage({})..setOrgId('123'); final context = SentryTransactionContext.fromSentryTrace( @@ -264,8 +263,7 @@ void main() { test('starts new trace when org IDs do not match', () { final options = defaultTestOptions()..orgId = '123'; - final header = - SentryTraceHeader(traceId, spanId, sampled: true); + final header = SentryTraceHeader(traceId, spanId, sampled: true); final baggage = SentryBaggage({})..setOrgId('456'); final context = SentryTransactionContext.fromSentryTrace( @@ -281,8 +279,7 @@ void main() { }); test('continues trace when options is null (backwards compat)', () { - final header = - SentryTraceHeader(traceId, spanId, sampled: true); + final header = SentryTraceHeader(traceId, spanId, sampled: true); final baggage = SentryBaggage({})..setOrgId('456'); final context = SentryTransactionContext.fromSentryTrace( @@ -301,8 +298,7 @@ void main() { final options = defaultTestOptions() ..orgId = '123' ..strictTraceContinuation = true; - final header = - SentryTraceHeader(traceId, spanId, sampled: true); + final header = SentryTraceHeader(traceId, spanId, sampled: true); final context = SentryTransactionContext.fromSentryTrace( 'name', @@ -318,8 +314,7 @@ void main() { test('starts new trace when SDK org ID is missing', () { final options = defaultTestOptions() ..strictTraceContinuation = true; - final header = - SentryTraceHeader(traceId, spanId, sampled: true); + final header = SentryTraceHeader(traceId, spanId, sampled: true); final baggage = SentryBaggage({})..setOrgId('123'); final context = SentryTransactionContext.fromSentryTrace( @@ -337,8 +332,7 @@ void main() { test('continues trace when both org IDs are missing', () { final options = defaultTestOptions() ..strictTraceContinuation = true; - final header = - SentryTraceHeader(traceId, spanId, sampled: true); + final header = SentryTraceHeader(traceId, spanId, sampled: true); final context = SentryTransactionContext.fromSentryTrace( 'name', From 80a652539480a9ede77bee84cf122db3ad3326e1 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 17 Mar 2026 15:33:49 +0100 Subject: [PATCH 3/8] fix: Apply dart format to test file --- .../test/strict_trace_continuation_test.dart | 36 ++++++++----------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/packages/dart/test/strict_trace_continuation_test.dart b/packages/dart/test/strict_trace_continuation_test.dart index a782b099e0..ce9153c1ab 100644 --- a/packages/dart/test/strict_trace_continuation_test.dart +++ b/packages/dart/test/strict_trace_continuation_test.dart @@ -49,16 +49,16 @@ void main() { test('returns DSN-extracted org id when orgId is not set', () { final options = SentryOptions( - dsn: 'https://public@o123.ingest.sentry.io/1') - ..automatedTestMode = true; + dsn: 'https://public@o123.ingest.sentry.io/1', + )..automatedTestMode = true; expect(options.effectiveOrgId, '123'); }); test('prefers explicit orgId over DSN-extracted org id', () { - final options = SentryOptions( - dsn: 'https://public@o123.ingest.sentry.io/1') - ..automatedTestMode = true - ..orgId = '456'; + final options = + SentryOptions(dsn: 'https://public@o123.ingest.sentry.io/1') + ..automatedTestMode = true + ..orgId = '456'; expect(options.effectiveOrgId, '456'); }); }); @@ -99,8 +99,7 @@ void main() { }); test('continues trace when SDK org ID is missing', () { - final options = defaultTestOptions() - ..strictTraceContinuation = false; + final options = defaultTestOptions()..strictTraceContinuation = false; expect(shouldContinueTrace(options, '123'), isTrue); }); }); @@ -114,14 +113,12 @@ void main() { }); test('starts new trace when SDK org ID is missing', () { - final options = defaultTestOptions() - ..strictTraceContinuation = true; + final options = defaultTestOptions()..strictTraceContinuation = true; expect(shouldContinueTrace(options, '123'), isFalse); }); test('continues trace when both org IDs are missing', () { - final options = defaultTestOptions() - ..strictTraceContinuation = true; + final options = defaultTestOptions()..strictTraceContinuation = true; expect(shouldContinueTrace(options, null), isTrue); }); @@ -155,8 +152,8 @@ void main() { test('setValuesFromScope includes org_id when available', () { final options = SentryOptions( - dsn: 'https://public@o123.ingest.sentry.io/1') - ..automatedTestMode = true; + dsn: 'https://public@o123.ingest.sentry.io/1', + )..automatedTestMode = true; final scope = Scope(options); final baggage = SentryBaggage({}); @@ -201,10 +198,7 @@ void main() { }); test('does not include orgId in toBaggage when null', () { - final context = SentryTraceContextHeader( - SentryId.newId(), - 'publicKey', - ); + final context = SentryTraceContextHeader(SentryId.newId(), 'publicKey'); final baggage = context.toBaggage(); expect(baggage.getOrgId(), isNull); }); @@ -312,8 +306,7 @@ void main() { }); test('starts new trace when SDK org ID is missing', () { - final options = defaultTestOptions() - ..strictTraceContinuation = true; + final options = defaultTestOptions()..strictTraceContinuation = true; final header = SentryTraceHeader(traceId, spanId, sampled: true); final baggage = SentryBaggage({})..setOrgId('123'); @@ -330,8 +323,7 @@ void main() { }); test('continues trace when both org IDs are missing', () { - final options = defaultTestOptions() - ..strictTraceContinuation = true; + final options = defaultTestOptions()..strictTraceContinuation = true; final header = SentryTraceHeader(traceId, spanId, sampled: true); final context = SentryTransactionContext.fromSentryTrace( From f96a7256df453a91b2d7577f1167b9ea13dbf38f Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 17 Mar 2026 15:45:24 +0100 Subject: [PATCH 4/8] fix: Remove unnecessary import flagged by dart analyzer --- packages/dart/test/strict_trace_continuation_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/dart/test/strict_trace_continuation_test.dart b/packages/dart/test/strict_trace_continuation_test.dart index ce9153c1ab..0defa1ee5a 100644 --- a/packages/dart/test/strict_trace_continuation_test.dart +++ b/packages/dart/test/strict_trace_continuation_test.dart @@ -1,5 +1,4 @@ import 'package:sentry/sentry.dart'; -import 'package:sentry/src/utils/tracing_utils.dart'; import 'package:test/test.dart'; import 'test_utils.dart'; From 7c249a7ee54b98d1f360b8c8ae4777cc181b3da9 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 17 Mar 2026 16:39:31 +0100 Subject: [PATCH 5/8] fix: Mark extractOrgIdFromDsnHost and shouldContinueTrace as @internal --- packages/dart/lib/src/protocol/dsn.dart | 8 +-- .../dart/lib/src/utils/tracing_utils.dart | 51 ++++++++++++------- 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/packages/dart/lib/src/protocol/dsn.dart b/packages/dart/lib/src/protocol/dsn.dart index 36641c8709..a9b38cb79e 100644 --- a/packages/dart/lib/src/protocol/dsn.dart +++ b/packages/dart/lib/src/protocol/dsn.dart @@ -7,6 +7,7 @@ final RegExp _orgIdFromHostRegExp = RegExp(r'^o(\d+)\.'); /// /// Returns the numeric org ID as a string, or `null` if the host does not /// match the expected pattern (e.g. `o123.ingest.sentry.io`). +@internal String? extractOrgIdFromDsnHost(String host) { final match = _orgIdFromHostRegExp.firstMatch(host); return match?.group(1); @@ -38,7 +39,8 @@ class Dsn { Uri get postUri { final uriCopy = uri!; - final port = uriCopy.hasPort && + final port = + uriCopy.hasPort && ((uriCopy.scheme == 'http' && uriCopy.port != 80) || (uriCopy.scheme == 'https' && uriCopy.port != 443)) ? ':${uriCopy.port}' @@ -49,8 +51,8 @@ class Dsn { String apiPath; if (pathLength > 1) { // some paths would present before the projectID in the uri - apiPath = - (uriCopy.pathSegments.sublist(0, pathLength - 1) + ['api']).join('/'); + apiPath = (uriCopy.pathSegments.sublist(0, pathLength - 1) + ['api']) + .join('/'); } else { apiPath = 'api'; } diff --git a/packages/dart/lib/src/utils/tracing_utils.dart b/packages/dart/lib/src/utils/tracing_utils.dart index b0d5528481..ae1cd9b5e2 100644 --- a/packages/dart/lib/src/utils/tracing_utils.dart +++ b/packages/dart/lib/src/utils/tracing_utils.dart @@ -1,7 +1,12 @@ +import 'package:meta/meta.dart'; + import '../../sentry.dart'; -SentryTraceHeader generateSentryTraceHeader( - {SentryId? traceId, SpanId? spanId, bool? sampled}) { +SentryTraceHeader generateSentryTraceHeader({ + SentryId? traceId, + SpanId? spanId, + bool? sampled, +}) { traceId ??= SentryId.newId(); spanId ??= SpanId.newId(); return SentryTraceHeader(traceId, spanId, sampled: sampled); @@ -17,10 +22,7 @@ void addTracingHeadersToHttpHeader( addW3CHeaderFromSpan(span, headers); } addSentryTraceHeaderFromSpan(span, headers); - addBaggageHeaderFromSpan( - span, - headers, - ); + addBaggageHeaderFromSpan(span, headers); } else { if (hub.options.propagateTraceparent) { addW3CHeaderFromScope(hub.scope, headers); @@ -37,18 +39,24 @@ void addSentryTraceHeaderFromScope(Scope scope, Map headers) { } void addSentryTraceHeaderFromSpan( - InstrumentationSpan span, Map headers) { + InstrumentationSpan span, + Map headers, +) { final traceHeader = span.toSentryTrace(); headers[traceHeader.name] = traceHeader.value; } void addSentryTraceHeader( - SentryTraceHeader traceHeader, Map headers) { + SentryTraceHeader traceHeader, + Map headers, +) { headers[traceHeader.name] = traceHeader.value; } void addW3CHeaderFromSpan( - InstrumentationSpan span, Map headers) { + InstrumentationSpan span, + Map headers, +) { final traceHeader = span.toSentryTrace(); _addW3CHeaderFromSentryTrace(traceHeader, headers); } @@ -60,7 +68,9 @@ void addW3CHeaderFromScope(Scope scope, Map headers) { } void _addW3CHeaderFromSentryTrace( - SentryTraceHeader traceHeader, Map headers) { + SentryTraceHeader traceHeader, + Map headers, +) { headers['traceparent'] = formatAsW3CHeader(traceHeader); } @@ -78,7 +88,9 @@ void addBaggageHeaderFromScope(Scope scope, Map headers) { } void addBaggageHeaderFromSpan( - InstrumentationSpan span, Map headers) { + InstrumentationSpan span, + Map headers, +) { final baggage = span.toBaggageHeader(); if (baggage != null) { addBaggageHeader(baggage, headers); @@ -86,15 +98,13 @@ void addBaggageHeaderFromSpan( } void addBaggageHeader( - SentryBaggageHeader baggage, Map headers) { + SentryBaggageHeader baggage, + Map headers, +) { final currentValue = headers[baggage.name] as String? ?? ''; - final currentBaggage = SentryBaggage.fromHeader( - currentValue, - ); - final sentryBaggage = SentryBaggage.fromHeader( - baggage.value, - ); + final currentBaggage = SentryBaggage.fromHeader(currentValue); + final sentryBaggage = SentryBaggage.fromHeader(baggage.value); // overwrite sentry's keys https://develop.sentry.dev/sdk/performance/dynamic-sampling-context/#baggage final filteredBaggageHeader = Map.from(currentBaggage.keyValues); @@ -111,7 +121,9 @@ void addBaggageHeader( } bool containsTargetOrMatchesRegExp( - List tracePropagationTargets, String url) { + List tracePropagationTargets, + String url, +) { if (tracePropagationTargets.isEmpty) { return false; } @@ -142,6 +154,7 @@ bool containsTargetOrMatchesRegExp( /// - Both org IDs present and different: new trace (always) /// - One or both missing, strict=false: continue /// - One or both missing, strict=true: new trace (unless both missing) +@internal bool shouldContinueTrace(SentryOptions options, String? baggageOrgId) { final sdkOrgId = options.effectiveOrgId; From e3bfa44c54aeb1524b42e99e38e0020815782f4b Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 17 Mar 2026 17:01:14 +0100 Subject: [PATCH 6/8] fix: Apply dart format with resolved dependencies --- packages/dart/lib/src/protocol/dsn.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/dart/lib/src/protocol/dsn.dart b/packages/dart/lib/src/protocol/dsn.dart index a9b38cb79e..edae16cbd8 100644 --- a/packages/dart/lib/src/protocol/dsn.dart +++ b/packages/dart/lib/src/protocol/dsn.dart @@ -39,8 +39,7 @@ class Dsn { Uri get postUri { final uriCopy = uri!; - final port = - uriCopy.hasPort && + final port = uriCopy.hasPort && ((uriCopy.scheme == 'http' && uriCopy.port != 80) || (uriCopy.scheme == 'https' && uriCopy.port != 443)) ? ':${uriCopy.port}' @@ -51,8 +50,8 @@ class Dsn { String apiPath; if (pathLength > 1) { // some paths would present before the projectID in the uri - apiPath = (uriCopy.pathSegments.sublist(0, pathLength - 1) + ['api']) - .join('/'); + apiPath = + (uriCopy.pathSegments.sublist(0, pathLength - 1) + ['api']).join('/'); } else { apiPath = 'api'; } From 8e0acf7f7d0642568a4268dd311abdeda0037e0c Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 17 Mar 2026 17:06:15 +0100 Subject: [PATCH 7/8] fix: Hide internal functions from public API exports --- packages/dart/lib/sentry.dart | 4 ++-- packages/dart/test/strict_trace_continuation_test.dart | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/dart/lib/sentry.dart b/packages/dart/lib/sentry.dart index 425e4cb01f..b84d98be51 100644 --- a/packages/dart/lib/sentry.dart +++ b/packages/dart/lib/sentry.dart @@ -22,7 +22,7 @@ export 'src/noop_isolate_error_integration.dart' if (dart.library.io) 'src/isolate_error_integration.dart'; // ignore: invalid_export_of_internal_element export 'src/performance_collector.dart'; -export 'src/protocol.dart'; +export 'src/protocol.dart' hide extractOrgIdFromDsnHost; export 'src/protocol/sentry_feature_flag.dart'; export 'src/protocol/sentry_feature_flags.dart'; export 'src/protocol/sentry_feedback.dart'; @@ -57,7 +57,7 @@ export 'src/utils.dart'; export 'src/utils/http_header_utils.dart'; // ignore: invalid_export_of_internal_element export 'src/utils/http_sanitizer.dart'; -export 'src/utils/tracing_utils.dart'; +export 'src/utils/tracing_utils.dart' hide shouldContinueTrace; // ignore: invalid_export_of_internal_element export 'src/utils/url_details.dart'; // ignore: invalid_export_of_internal_element diff --git a/packages/dart/test/strict_trace_continuation_test.dart b/packages/dart/test/strict_trace_continuation_test.dart index 0defa1ee5a..857290bd70 100644 --- a/packages/dart/test/strict_trace_continuation_test.dart +++ b/packages/dart/test/strict_trace_continuation_test.dart @@ -1,4 +1,6 @@ import 'package:sentry/sentry.dart'; +import 'package:sentry/src/protocol/dsn.dart'; +import 'package:sentry/src/utils/tracing_utils.dart'; import 'package:test/test.dart'; import 'test_utils.dart'; From e3d200ad07c37fa266ffc985110165b457867fc1 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 17 Mar 2026 17:09:36 +0100 Subject: [PATCH 8/8] fix: Add direct dsn.dart import to sentry_options.dart --- packages/dart/lib/src/sentry_options.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/dart/lib/src/sentry_options.dart b/packages/dart/lib/src/sentry_options.dart index 6713857092..447becca82 100644 --- a/packages/dart/lib/src/sentry_options.dart +++ b/packages/dart/lib/src/sentry_options.dart @@ -5,6 +5,7 @@ import 'package:meta/meta.dart'; import '../sentry.dart'; import 'client_reports/client_report_recorder.dart'; +import 'protocol/dsn.dart'; import 'client_reports/noop_client_report_recorder.dart'; import 'diagnostic_log.dart'; import 'environment/environment_variables.dart';