Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
7e891ab
feat: Implement strict trace continuation
antonis Mar 17, 2026
a48e7d7
fix: Apply dart format and update changelog PR number
antonis Mar 17, 2026
80a6525
fix: Apply dart format to test file
antonis Mar 17, 2026
f96a725
fix: Remove unnecessary import flagged by dart analyzer
antonis Mar 17, 2026
7c249a7
fix: Mark extractOrgIdFromDsnHost and shouldContinueTrace as @internal
antonis Mar 17, 2026
e3bfa44
fix: Apply dart format with resolved dependencies
antonis Mar 17, 2026
8e0acf7
fix: Hide internal functions from public API exports
antonis Mar 17, 2026
e3d200a
fix: Add direct dsn.dart import to sentry_options.dart
antonis Mar 17, 2026
7806294
Merge remote-tracking branch 'origin/main' into feat/strict-trace-conโ€ฆ
antonis Apr 16, 2026
c78ff38
chore: Move changelog entry to Unreleased section
antonis Apr 16, 2026
d2b12c7
Merge remote-tracking branch 'origin/main' into feat/strict-trace-conโ€ฆ
antonis Apr 22, 2026
c15cb8f
feat(flutter): Pass strict trace continuation options to native Android
antonis Apr 22, 2026
75f8ab2
fix(flutter): Use effectiveOrgId on native Android and bump size threโ€ฆ
antonis Apr 22, 2026
25b589e
style: Wrap long line for dart format
antonis Apr 22, 2026
7d2e25c
fix: Propagate orgId in SentryTraceContextHeader.fromRecordingSpan
antonis Apr 22, 2026
a1d3441
fix: Tighten size threshold and ignore internal member warning
antonis Apr 22, 2026
9c81fe0
style: Revert unrelated formatting changes in tracing_utils.dart
antonis Apr 22, 2026
4afe986
style: Align internal exports with codebase convention
antonis Apr 22, 2026
55d5089
feat: Harden org ID handling and auto-resolve options in fromSentryTrace
antonis Apr 22, 2026
223b1b3
docs: Document fromSentryTrace validation and options fallback
antonis Apr 22, 2026
d80a407
Merge branch 'main' into feat/strict-trace-continuation
antonis Apr 24, 2026
644dcd3
Merge branch 'main' into feat/strict-trace-continuation
antonis Apr 29, 2026
02ef65b
Use safe method for org_id
antonis Apr 30, 2026
168eb6f
Remove unneeded comments
antonis Apr 30, 2026
48e3d72
Merge branch 'main' into feat/strict-trace-continuation
buenaflor Apr 30, 2026
e9f7fc8
Remove unneeded comments
antonis Apr 30, 2026
65b1c56
Remove changelog
antonis Apr 30, 2026
60de705
fix: Add missing import for getValueOrNull extension method
antonis Apr 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion metrics/metrics-android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ startupTimeTest:

binarySizeTest:
diffMin: 900 KiB
diffMax: 1300 KiB
diffMax: 1350 KiB
Copy link
Copy Markdown
Contributor Author

@antonis antonis Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes led to ~1345 KiB, exceeding the 1300 KiB threshold

2 changes: 2 additions & 0 deletions packages/dart/lib/sentry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +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';
// ignore: invalid_export_of_internal_element
export 'src/protocol.dart';
export 'src/protocol/sentry_feature_flag.dart';
export 'src/protocol/sentry_feature_flags.dart';
Expand Down Expand Up @@ -59,6 +60,7 @@ export 'src/utils.dart';
export 'src/utils/http_header_utils.dart';
// ignore: invalid_export_of_internal_element
export 'src/utils/http_sanitizer.dart';
// ignore: invalid_export_of_internal_element
export 'src/utils/tracing_utils.dart';
// ignore: invalid_export_of_internal_element
export 'src/utils/url_details.dart';
Expand Down
13 changes: 13 additions & 0 deletions packages/dart/lib/src/protocol/dsn.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
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`).
@internal
String? extractOrgIdFromDsnHost(String host) {
final match = _orgIdFromHostRegExp.firstMatch(host);
return match?.group(1);
Comment thread
cursor[bot] marked this conversation as resolved.
}

/// The Data Source Name (DSN) tells the SDK where to send the events
@immutable
class Dsn {
Expand Down
10 changes: 10 additions & 0 deletions packages/dart/lib/src/sentry_baggage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String> _extractKeyValuesFromBaggageString(
Expand Down Expand Up @@ -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() {
Expand Down
37 changes: 37 additions & 0 deletions packages/dart/lib/src/sentry_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,43 @@ 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.
///
/// Empty or whitespace-only explicit [orgId] values are treated as unset
/// and fall back to the DSN.
@internal
String? get effectiveOrgId {
final explicit = orgId?.trim();
if (explicit != null && explicit.isNotEmpty) {
return explicit;
}
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();

Expand Down
12 changes: 12 additions & 0 deletions packages/dart/lib/src/sentry_trace_context_header.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:meta/meta.dart';
import '../sentry.dart';
import 'protocol/access_aware_map.dart';
import 'utils/sample_rate_format.dart';
import 'utils/type_safe_map_access.dart';

class SentryTraceContextHeader {
SentryTraceContextHeader(
Expand All @@ -17,6 +18,7 @@ class SentryTraceContextHeader {
this.sampled,
this.unknown,
this.replayId,
this.orgId,
});

final SentryId traceId;
Expand All @@ -35,6 +37,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<String, dynamic> data) {
final json = AccessAwareMap(data);
Expand All @@ -49,6 +54,7 @@ class SentryTraceContextHeader {
sampled: json['sampled'],
replayId:
json['replay_id'] == null ? null : SentryId.fromId(json['replay_id']),
orgId: json.getValueOrNull('org_id'),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the import is missing because getValueOrNull is an extension method

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added with 60de705

unknown: json.notAccessed(),
);
}
Expand All @@ -66,6 +72,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,
};
}

Expand Down Expand Up @@ -98,6 +105,9 @@ class SentryTraceContextHeader {
if (replayId != null) {
baggage.setReplayId(replayId.toString());
}
if (orgId != null) {
baggage.setOrgId(orgId!);
}
return baggage;
}

Expand All @@ -109,6 +119,7 @@ class SentryTraceContextHeader {
release: baggage.get('sentry-release'),
environment: baggage.get('sentry-environment'),
replayId: baggage.getReplayId(),
orgId: baggage.getOrgId(),
Comment thread
antonis marked this conversation as resolved.
);
}

Expand All @@ -134,6 +145,7 @@ class SentryTraceContextHeader {
sampleRand: span.samplingDecision.sampleRand?.toString(),
sampled: span.samplingDecision.sampled.toString(),
replayId: replayId,
orgId: options.effectiveOrgId,
);
}
}
1 change: 1 addition & 0 deletions packages/dart/lib/src/sentry_tracer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,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;
Expand Down
24 changes: 24 additions & 0 deletions packages/dart/lib/src/sentry_transaction_context.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import 'protocol.dart';
import 'sentry.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;
Expand All @@ -24,13 +27,34 @@ class SentryTransactionContext extends SentrySpanContext {
operation: operation,
);

/// Creates a [SentryTransactionContext] from an incoming [traceHeader] and
/// optional [baggage].
///
/// Validates the incoming trace's `sentry-org_id` against the SDK's
/// organization ID (see [SentryOptions.strictTraceContinuation]). When the
/// trace should not be continued, a new trace is started instead.
///
/// If [options] is not provided, the current hub's options are used.
factory SentryTransactionContext.fromSentryTrace(
String name,
String operation,
SentryTraceHeader traceHeader, {
SentryTransactionNameSource? transactionNameSource,
SentryBaggage? baggage,
SentryOptions? options,
}) {
final effectiveOptions = options ?? Sentry.currentHub.options;

if (!shouldContinueTrace(effectiveOptions, baggage?.getOrgId())) {
return SentryTransactionContext(
name,
operation,
transactionNameSource:
transactionNameSource ?? SentryTransactionNameSource.custom,
origin: SentryTraceOrigins.manual,
);
}

final sampleRate = baggage?.getSampleRate();
final sampleRand = baggage?.getSampleRand();
return SentryTransactionContext(
Expand Down
49 changes: 49 additions & 0 deletions packages/dart/lib/src/utils/tracing_utils.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'package:meta/meta.dart';

import '../../sentry.dart';

SentryTraceHeader generateSentryTraceHeader(
Expand Down Expand Up @@ -132,6 +134,53 @@ 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)
@internal
bool shouldContinueTrace(SentryOptions options, String? baggageOrgId) {
final sdkOrgId = options.effectiveOrgId;
// Treat empty/whitespace-only baggage org IDs as absent
final trimmedBaggageOrgId = baggageOrgId?.trim();
baggageOrgId = (trimmedBaggageOrgId != null && trimmedBaggageOrgId.isNotEmpty)
? trimmedBaggageOrgId
: null;

// 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) {
if (sdkOrgId == null && baggageOrgId == null) {
return true;
}
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;
Expand Down
Loading
Loading