Skip to content

Commit 6f47800

Browse files
Implement sampled flag in propagation context (#3084)
* Implement trace sampling propagation across transactions Co-authored-by: giancarlo.buenaflor <giancarlo.buenaflor@sentry.io> * Update hub.dart * Update * Update * Update hub_test.dart * Update hub_test.dart * Update hub.dart * Update hub.dart * Update * Update * Update CHANGELOG * Fix analyze * Update * Update --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent ab702ca commit 6f47800

File tree

6 files changed

+207
-52
lines changed

6 files changed

+207
-52
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313

1414
- Ensure consistent sampling per trace ([#3079](https://github.com/getsentry/sentry-dart/pull/3079))
1515

16+
### Enhancements
17+
18+
- Add sampled flag in propagation context ([#3084](https://github.com/getsentry/sentry-dart/pull/3084))
19+
1620
### Dependencies
1721

1822
- Bump Native SDK from v0.9.0 to v0.9.1 ([#3018](https://github.com/getsentry/sentry-dart/pull/3018))

dart/lib/src/hub.dart

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,11 @@ class Hub {
515515
transactionContext.origin ??= SentryTraceOrigins.manual;
516516
transactionContext.traceId = propagationContext.traceId;
517517

518+
// Persist the "sampled" decision onto the propagation context the
519+
// first time we obtain one for the current trace.
520+
// Subsequent transactions do not affect the sampled flag.
521+
propagationContext.applySamplingDecision(samplingDecision.sampled);
522+
518523
SentryProfiler? profiler;
519524
if (_profilerFactory != null &&
520525
_tracesSampler.sampleProfiling(samplingDecision)) {
@@ -543,9 +548,9 @@ class Hub {
543548

544549
@internal
545550
void generateNewTrace() {
546-
scope.propagationContext.traceId = SentryId.newId();
547-
// Reset sampleRand so that a new one is generated for the new trace when a new transaction is started
548-
scope.propagationContext.sampleRand = null;
551+
// Create a brand-new trace and reset the sampling flag and sampleRand so
552+
// that the next root transaction can set it again.
553+
scope.propagationContext.resetTrace();
549554
}
550555

551556
/// Gets the current active transaction or span.

dart/lib/src/propagation_context.dart

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,48 @@ class PropagationContext {
1010
/// The dynamic sampling context.
1111
SentryBaggage? baggage;
1212

13+
bool? _sampled;
14+
15+
/// Indicates whether the current trace is sampled or not.
16+
///
17+
/// This flag follows the lifecycle of a trace:
18+
/// * It starts as `null` (undecided).
19+
/// * The **first** transaction that receives a sampling decision (root
20+
/// transaction) sets the flag to the decided value. Subsequent
21+
/// transactions for the same trace MUST NOT change the value.
22+
/// * When a new trace is started (i.e. when a new `traceId` is generated),
23+
/// the flag is reset back to `null`.
24+
///
25+
/// The flag is propagated via the `sentry-trace` header so that downstream
26+
/// services can honour the original sampling decision.
27+
bool? get sampled => _sampled;
28+
29+
/// Applies the sampling decision exactly once per trace.
30+
void applySamplingDecision(bool sampled) {
31+
_sampled ??= sampled;
32+
}
33+
1334
/// Random number generated for sampling decisions.
1435
///
1536
/// This value must be generated **once per trace** and reused across all
1637
/// child spans and transactions that belong to the same trace. It is reset
1738
/// whenever a new trace is started.
1839
double? sampleRand;
1940

41+
/// Starts a brand-new trace (new ID, new sampling value & sampled state).
42+
void resetTrace() {
43+
traceId = SentryId.newId();
44+
sampleRand = null;
45+
_sampled = null;
46+
}
47+
2048
/// Baggage header to attach to http headers.
2149
SentryBaggageHeader? toBaggageHeader() =>
2250
baggage != null ? SentryBaggageHeader.fromBaggage(baggage!) : null;
2351

2452
/// Sentry trace header to attach to http headers.
25-
SentryTraceHeader toSentryTrace() =>
26-
generateSentryTraceHeader(traceId: traceId);
53+
SentryTraceHeader toSentryTrace() => generateSentryTraceHeader(
54+
traceId: traceId,
55+
sampled: sampled,
56+
);
2757
}

dart/lib/src/protocol/sentry_trace_context.dart

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,11 @@ class SentryTraceContext {
118118
factory SentryTraceContext.fromPropagationContext(
119119
PropagationContext propagationContext) {
120120
return SentryTraceContext(
121-
traceId: propagationContext.traceId,
122-
spanId: SpanId.newId(),
123-
operation: 'default',
124-
replayId: propagationContext.baggage?.getReplayId());
121+
traceId: propagationContext.traceId,
122+
spanId: SpanId.newId(),
123+
operation: 'default',
124+
sampled: propagationContext.sampled,
125+
replayId: propagationContext.baggage?.getReplayId(),
126+
);
125127
}
126128
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import 'package:sentry/sentry.dart';
2+
import 'package:sentry/src/sentry_tracer.dart';
3+
import 'package:test/test.dart';
4+
5+
import 'test_utils.dart';
6+
7+
void main() {
8+
group('PropagationContext', () {
9+
group('traceId', () {
10+
test('is a new trace id by default', () {
11+
final hub = Hub(defaultTestOptions());
12+
final sut = hub.scope.propagationContext;
13+
final traceId = sut.traceId;
14+
expect(traceId, isNotNull);
15+
});
16+
17+
test('is reused for transactions within the same trace', () {
18+
final options = defaultTestOptions()..tracesSampleRate = 1.0;
19+
final hub = Hub(options);
20+
final sut = hub.scope.propagationContext;
21+
22+
final tx1 = hub.startTransaction('tx1', 'op') as SentryTracer;
23+
final traceId1 = sut.traceId;
24+
25+
final tx2 = hub.startTransaction('tx2', 'op') as SentryTracer;
26+
final traceId2 = sut.traceId;
27+
28+
expect(tx1.context.traceId, equals(tx2.context.traceId));
29+
expect(tx1.context.traceId, equals(traceId1));
30+
expect(traceId1, equals(traceId2));
31+
});
32+
});
33+
34+
group('sampleRand', () {
35+
test('is null by default', () {
36+
final hub = Hub(defaultTestOptions());
37+
final sut = hub.scope.propagationContext;
38+
final sampleRand = sut.sampleRand;
39+
expect(sampleRand, isNull);
40+
});
41+
42+
test('is set by the first transaction and stays unchanged', () {
43+
final options = defaultTestOptions()..tracesSampleRate = 1.0;
44+
final hub = Hub(options);
45+
final sut = hub.scope.propagationContext;
46+
47+
final tx1 = hub.startTransaction('tx1', 'op') as SentryTracer;
48+
final rand1 = tx1.samplingDecision?.sampleRand;
49+
expect(rand1, isNotNull);
50+
51+
final tx2 = hub.startTransaction('tx2', 'op') as SentryTracer;
52+
final rand2 = tx2.samplingDecision?.sampleRand;
53+
54+
expect(rand2, equals(rand1));
55+
expect(rand1, equals(sut.sampleRand));
56+
});
57+
});
58+
59+
group('sampled', () {
60+
test('is null by default', () {
61+
final hub = Hub(defaultTestOptions()..tracesSampleRate = 1.0);
62+
final sut = hub.scope.propagationContext;
63+
expect(sut.sampled, isNull);
64+
});
65+
66+
test('is set by the first transaction and stays unchanged', () {
67+
final hub = Hub(defaultTestOptions()..tracesSampleRate = 1.0);
68+
final sut = hub.scope.propagationContext;
69+
// 1. Start the first (root) transaction with an explicit sampled = true.
70+
final txContextTrue = SentryTransactionContext(
71+
'trx',
72+
'op',
73+
samplingDecision: SentryTracesSamplingDecision(true),
74+
);
75+
hub.startTransactionWithContext(txContextTrue);
76+
77+
expect(sut.sampled, isTrue);
78+
79+
// 2. Start a second transaction with sampled = false – the flag must not change.
80+
final txContextFalse = SentryTransactionContext(
81+
'trx-2',
82+
'op',
83+
samplingDecision: SentryTracesSamplingDecision(false),
84+
);
85+
hub.startTransactionWithContext(txContextFalse);
86+
87+
expect(sut.sampled, isTrue,
88+
reason: 'sampled flag must remain unchanged for the trace');
89+
});
90+
91+
test('is reset when a new trace is generated', () {
92+
final hub = Hub(defaultTestOptions()..tracesSampleRate = 1.0);
93+
final sut = hub.scope.propagationContext;
94+
final txContext = SentryTransactionContext(
95+
'trx',
96+
'op',
97+
samplingDecision: SentryTracesSamplingDecision(true),
98+
);
99+
hub.startTransactionWithContext(txContext);
100+
expect(sut.sampled, isTrue);
101+
102+
// Simulate new trace.
103+
hub.generateNewTrace();
104+
expect(sut.sampled, isNull);
105+
});
106+
107+
test('applySamplingDecision only sets sampled flag once', () {
108+
final hub = Hub(defaultTestOptions()..tracesSampleRate = 1.0);
109+
final sut = hub.scope.propagationContext;
110+
111+
expect(sut.sampled, isNull);
112+
sut.applySamplingDecision(true);
113+
expect(sut.sampled, isTrue);
114+
sut.applySamplingDecision(false);
115+
expect(sut.sampled, isTrue);
116+
});
117+
});
118+
119+
group('resetTrace', () {
120+
test('resets values', () {
121+
final hub = Hub(defaultTestOptions()..tracesSampleRate = 1.0);
122+
final sut = hub.scope.propagationContext;
123+
124+
final traceId = SentryId.newId();
125+
sut.traceId = traceId;
126+
sut.sampleRand = 1.0;
127+
sut.applySamplingDecision(true);
128+
129+
sut.resetTrace();
130+
131+
expect(sut.traceId, isNot(traceId));
132+
expect(sut.sampleRand, isNull);
133+
expect(sut.sampled, isNull);
134+
});
135+
});
136+
137+
group('toSentryTrace', () {
138+
test('header reflects values', () {
139+
final options = defaultTestOptions()..tracesSampleRate = 1.0;
140+
final hub = Hub(options);
141+
final sut = hub.scope.propagationContext;
142+
143+
final txContext = SentryTransactionContext(
144+
'trx',
145+
'op',
146+
samplingDecision: SentryTracesSamplingDecision(true),
147+
);
148+
hub.startTransactionWithContext(txContext);
149+
150+
final header = sut.toSentryTrace();
151+
expect(header.sampled, isTrue);
152+
expect(header.value.split('-').length, 3,
153+
reason: 'header must contain the sampled decision');
154+
});
155+
});
156+
});
157+
}

dart/test/sample_rand_propagation_test.dart

Lines changed: 0 additions & 43 deletions
This file was deleted.

0 commit comments

Comments
 (0)