@@ -129,61 +129,72 @@ internal ITransactionTracer StartTransaction(
129
129
IReadOnlyDictionary < string , object ? > customSamplingContext ,
130
130
DynamicSamplingContext ? dynamicSamplingContext )
131
131
{
132
- var transaction = new TransactionTracer ( this , context )
133
- {
134
- SampleRand = dynamicSamplingContext ? . Items . TryGetValue ( "sample_rand" , out var sampleRand ) ?? false
135
- ? double . Parse ( sampleRand , NumberStyles . Float , CultureInfo . InvariantCulture )
136
- : SampleRandHelper . GenerateSampleRand ( context . TraceId . ToString ( ) )
137
- } ;
138
-
139
132
// If the hub is disabled, we will always sample out. In other words, starting a transaction
140
133
// after disposing the hub will result in that transaction not being sent to Sentry.
141
- // Additionally, we will always sample out if tracing is explicitly disabled.
142
- // Do not invoke the TracesSampler, evaluate the TracesSampleRate, and override any sampling decision
143
- // that may have been already set (i.e.: from a sentry-trace header).
144
134
if ( ! IsEnabled )
145
135
{
146
- transaction . IsSampled = false ;
147
- transaction . SampleRate = 0.0 ;
136
+ return NoOpTransaction . Instance ;
148
137
}
149
- else
150
- {
151
- // Except when tracing is disabled, TracesSampler runs regardless of whether a decision
152
- // has already been made, as it can be used to override it.
153
- if ( _options . TracesSampler is { } tracesSampler )
154
- {
155
- var samplingContext = new TransactionSamplingContext (
156
- context ,
157
- customSamplingContext ) ;
158
138
159
- if ( tracesSampler ( samplingContext ) is { } sampleRate )
160
- {
161
- transaction . IsSampled = SampleRandHelper . IsSampled ( transaction . SampleRand . Value , sampleRate ) ;
162
- transaction . SampleRate = sampleRate ;
163
- }
164
- }
139
+ bool ? isSampled = null ;
140
+ double ? sampleRate = null ;
141
+ var sampleRand = dynamicSamplingContext ? . Items . TryGetValue ( "sample_rand" , out var dscsampleRand ) ?? false
142
+ ? double . Parse ( dscsampleRand , NumberStyles . Float , CultureInfo . InvariantCulture )
143
+ : SampleRandHelper . GenerateSampleRand ( context . TraceId . ToString ( ) ) ;
165
144
166
- // Random sampling runs only if the sampling decision hasn't been made already.
167
- if ( transaction . IsSampled == null )
145
+ // TracesSampler runs regardless of whether a decision has already been made, as it can be used to override it.
146
+ if ( _options . TracesSampler is { } tracesSampler )
147
+ {
148
+ var samplingContext = new TransactionSamplingContext (
149
+ context ,
150
+ customSamplingContext ) ;
151
+
152
+ if ( tracesSampler ( samplingContext ) is { } samplerSampleRate )
168
153
{
169
- var sampleRate = _options . TracesSampleRate ?? 0.0 ;
170
- transaction . IsSampled = SampleRandHelper . IsSampled ( transaction . SampleRand . Value , sampleRate ) ;
171
- transaction . SampleRate = sampleRate ;
154
+ // The TracesSampler trumps all other sampling decisions (even the trace header)
155
+ sampleRate = samplerSampleRate ;
156
+ isSampled = SampleRandHelper . IsSampled ( sampleRand , sampleRate . Value ) ;
172
157
}
158
+ }
173
159
174
- if ( transaction . IsSampled is true &&
175
- _options . TransactionProfilerFactory is { } profilerFactory &&
176
- _randomValuesFactory . NextBool ( _options . ProfilesSampleRate ?? 0.0 ) )
160
+ // If the sampling decision isn't made by a trace sampler we check the trace header first (from the context) or
161
+ // finally fallback to Random sampling if the decision has been made by no other means
162
+ sampleRate ??= _options . TracesSampleRate ?? 0.0 ;
163
+ isSampled ??= context . IsSampled ?? SampleRandHelper . IsSampled ( sampleRand , sampleRate . Value ) ;
164
+
165
+ // Make sure there is a replayId (if available) on the provided DSC (if any).
166
+ dynamicSamplingContext = dynamicSamplingContext ? . WithReplayId ( _replaySession ) ;
167
+
168
+ if ( isSampled is false )
169
+ {
170
+ var unsampledTransaction = new UnsampledTransaction ( this , context )
177
171
{
178
- // TODO cancellation token based on Hub being closed?
179
- transaction . TransactionProfiler = profilerFactory . Start ( transaction , CancellationToken . None ) ;
180
- }
172
+ SampleRate = sampleRate ,
173
+ SampleRand = sampleRand ,
174
+ DynamicSamplingContext = dynamicSamplingContext // Default to the provided DSC
175
+ } ;
176
+ // If no DSC was provided, create one based on this transaction.
177
+ // Must be done AFTER the sampling decision has been made (the DSC propagates sampling decisions).
178
+ unsampledTransaction . DynamicSamplingContext ??= unsampledTransaction . CreateDynamicSamplingContext ( _options , _replaySession ) ;
179
+ return unsampledTransaction ;
181
180
}
182
181
183
- // Use the provided DSC (adding the active replayId if necessary), or create one based on this transaction.
184
- // DSC creation must be done AFTER the sampling decision has been made.
185
- transaction . DynamicSamplingContext = dynamicSamplingContext ? . WithReplayId ( _replaySession )
186
- ?? transaction . CreateDynamicSamplingContext ( _options , _replaySession ) ;
182
+ var transaction = new TransactionTracer ( this , context )
183
+ {
184
+ SampleRate = sampleRate ,
185
+ SampleRand = sampleRand ,
186
+ DynamicSamplingContext = dynamicSamplingContext // Default to the provided DSC
187
+ } ;
188
+ // If no DSC was provided, create one based on this transaction.
189
+ // Must be done AFTER the sampling decision has been made (the DSC propagates sampling decisions).
190
+ transaction . DynamicSamplingContext ??= transaction . CreateDynamicSamplingContext ( _options , _replaySession ) ;
191
+
192
+ if ( _options . TransactionProfilerFactory is { } profilerFactory &&
193
+ _randomValuesFactory . NextBool ( _options . ProfilesSampleRate ?? 0.0 ) )
194
+ {
195
+ // TODO cancellation token based on Hub being closed?
196
+ transaction . TransactionProfiler = profilerFactory . Start ( transaction , CancellationToken . None ) ;
197
+ }
187
198
188
199
// A sampled out transaction still appears fully functional to the user
189
200
// but will be dropped by the client and won't reach Sentry's servers.
@@ -220,7 +231,7 @@ public SentryTraceHeader GetTraceHeader()
220
231
public BaggageHeader GetBaggage ( )
221
232
{
222
233
var span = GetSpan ( ) ;
223
- if ( span ? . GetTransaction ( ) is TransactionTracer { DynamicSamplingContext : { IsEmpty : false } dsc } )
234
+ if ( span ? . GetTransaction ( ) . GetDynamicSamplingContext ( ) is { IsEmpty : false } dsc )
224
235
{
225
236
return dsc . ToBaggageHeader ( ) ;
226
237
}
@@ -373,9 +384,9 @@ private void ApplyTraceContextToEvent(SentryEvent evt, ISpan span)
373
384
evt . Contexts . Trace . TraceId = span . TraceId ;
374
385
evt . Contexts . Trace . ParentSpanId = span . ParentSpanId ;
375
386
376
- if ( span . GetTransaction ( ) is TransactionTracer transactionTracer )
387
+ if ( span . GetTransaction ( ) . GetDynamicSamplingContext ( ) is { } dsc )
377
388
{
378
- evt . DynamicSamplingContext = transactionTracer . DynamicSamplingContext ;
389
+ evt . DynamicSamplingContext = dsc ;
379
390
}
380
391
}
381
392
0 commit comments