Skip to content

Commit dd0451c

Browse files
WIP
1 parent ee62bb9 commit dd0451c

File tree

4 files changed

+132
-44
lines changed

4 files changed

+132
-44
lines changed

src/Sentry/Internal/Hub.cs

Lines changed: 36 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -127,52 +127,55 @@ internal ITransactionTracer StartTransaction(
127127
IReadOnlyDictionary<string, object?> customSamplingContext,
128128
DynamicSamplingContext? dynamicSamplingContext)
129129
{
130-
var transaction = new TransactionTracer(this, context);
131-
132130
// If the hub is disabled, we will always sample out. In other words, starting a transaction
133131
// after disposing the hub will result in that transaction not being sent to Sentry.
134132
// Additionally, we will always sample out if tracing is explicitly disabled.
135133
// Do not invoke the TracesSampler, evaluate the TracesSampleRate, and override any sampling decision
136134
// that may have been already set (i.e.: from a sentry-trace header).
137135
if (!IsEnabled)
138136
{
139-
transaction.IsSampled = false;
140-
transaction.SampleRate = 0.0;
137+
return NoOpTransaction.Instance;
141138
}
142-
else
143-
{
144-
// Except when tracing is disabled, TracesSampler runs regardless of whether a decision
145-
// 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);
151139

152-
if (tracesSampler(samplingContext) is { } sampleRate)
153-
{
154-
transaction.IsSampled = _randomValuesFactory.NextBool(sampleRate);
155-
transaction.SampleRate = sampleRate;
156-
}
157-
}
140+
double? sampleRate = null;
158141

159-
// Random sampling runs only if the sampling decision hasn't been made already.
160-
if (transaction.IsSampled == null)
161-
{
162-
var sampleRate = _options.TracesSampleRate ?? 0.0;
163-
transaction.IsSampled = _randomValuesFactory.NextBool(sampleRate);
164-
transaction.SampleRate = sampleRate;
165-
}
142+
// Except when tracing is disabled, TracesSampler runs regardless of whether a decision
143+
// has already been made, as it can be used to override it.
144+
if (_options.TracesSampler is { } tracesSampler)
145+
{
146+
var samplingContext = new TransactionSamplingContext(
147+
context,
148+
customSamplingContext);
166149

167-
if (transaction.IsSampled is true &&
168-
_options.TransactionProfilerFactory is { } profilerFactory &&
169-
_randomValuesFactory.NextBool(_options.ProfilesSampleRate ?? 0.0))
150+
if (tracesSampler(samplingContext) is { } samplerSampleRate)
170151
{
171-
// TODO cancellation token based on Hub being closed?
172-
transaction.TransactionProfiler = profilerFactory.Start(transaction, CancellationToken.None);
152+
sampleRate = samplerSampleRate;
173153
}
174154
}
175155

156+
// If the sampling decision isn't made by a trace sampler then fallback to Random sampling
157+
sampleRate ??= _options.TracesSampleRate ?? 0.0;
158+
159+
var isSampled = _randomValuesFactory.NextBool(sampleRate.Value);
160+
if (!isSampled)
161+
{
162+
// var unsampledTransaction = new UnsampledTransaction(this, context);
163+
// return unsampledTransaction;
164+
return new UnsampledTransaction(this, context);
165+
}
166+
167+
var transaction = new TransactionTracer(this, context)
168+
{
169+
IsSampled = true,
170+
SampleRate = sampleRate
171+
};
172+
if (_options.TransactionProfilerFactory is { } profilerFactory &&
173+
_randomValuesFactory.NextBool(_options.ProfilesSampleRate ?? 0.0))
174+
{
175+
// TODO cancellation token based on Hub being closed?
176+
transaction.TransactionProfiler = profilerFactory.Start(transaction, CancellationToken.None);
177+
}
178+
176179
// Use the provided DSC, or create one based on this transaction.
177180
// DSC creation must be done AFTER the sampling decision has been made.
178181
transaction.DynamicSamplingContext =
@@ -213,6 +216,8 @@ public SentryTraceHeader GetTraceHeader()
213216
public BaggageHeader GetBaggage()
214217
{
215218
var span = GetSpan();
219+
// TODO: Things like the SampleRand won't get propagated unless we get them from a DSC
220+
// ... so we'd need to get these from an UnsampledTransaction as well as a TransactionTracer
216221
if (span?.GetTransaction() is TransactionTracer { DynamicSamplingContext: { IsEmpty: false } dsc })
217222
{
218223
return dsc.ToBaggageHeader();

src/Sentry/Internal/NoOpSpan.cs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,18 @@ protected NoOpSpan()
1313
{
1414
}
1515

16-
public SpanId SpanId => SpanId.Empty;
16+
public virtual SpanId SpanId => SpanId.Empty;
1717
public SpanId? ParentSpanId => SpanId.Empty;
18-
public SentryId TraceId => SentryId.Empty;
19-
public bool? IsSampled => default;
18+
public virtual SentryId TraceId => SentryId.Empty;
19+
public virtual bool? IsSampled => default;
2020
public IReadOnlyDictionary<string, string> Tags => ImmutableDictionary<string, string>.Empty;
2121
public IReadOnlyDictionary<string, object?> Extra => ImmutableDictionary<string, object?>.Empty;
2222
public IReadOnlyDictionary<string, object?> Data => ImmutableDictionary<string, object?>.Empty;
2323
public DateTimeOffset StartTimestamp => default;
2424
public DateTimeOffset? EndTimestamp => default;
2525
public bool IsFinished => default;
2626

27-
public string Operation
27+
public virtual string Operation
2828
{
2929
get => string.Empty;
3030
set { }
@@ -42,21 +42,21 @@ public SpanStatus? Status
4242
set { }
4343
}
4444

45-
public ISpan StartChild(string operation) => this;
45+
public virtual ISpan StartChild(string operation) => this;
4646

47-
public void Finish()
47+
public virtual void Finish()
4848
{
4949
}
5050

51-
public void Finish(SpanStatus status)
51+
public virtual void Finish(SpanStatus status)
5252
{
5353
}
5454

55-
public void Finish(Exception exception, SpanStatus status)
55+
public virtual void Finish(Exception exception, SpanStatus status)
5656
{
5757
}
5858

59-
public void Finish(Exception exception)
59+
public virtual void Finish(Exception exception)
6060
{
6161
}
6262

@@ -76,7 +76,7 @@ public void SetData(string key, object? value)
7676
{
7777
}
7878

79-
public SentryTraceHeader GetTraceHeader() => SentryTraceHeader.Empty;
79+
public virtual SentryTraceHeader GetTraceHeader() => SentryTraceHeader.Empty;
8080

8181
public IReadOnlyDictionary<string, Measurement> Measurements => ImmutableDictionary<string, Measurement>.Empty;
8282

src/Sentry/Internal/NoOpTransaction.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ internal class NoOpTransaction : NoOpSpan, ITransactionTracer
77
{
88
public new static ITransactionTracer Instance { get; } = new NoOpTransaction();
99

10-
private NoOpTransaction()
10+
protected NoOpTransaction()
1111
{
1212
}
1313

1414
public SdkVersion Sdk => SdkVersion.Instance;
1515

16-
public string Name
16+
public virtual string Name
1717
{
1818
get => string.Empty;
1919
set { }
@@ -87,7 +87,7 @@ public IReadOnlyList<string> Fingerprint
8787
set { }
8888
}
8989

90-
public IReadOnlyCollection<ISpan> Spans => ImmutableList<ISpan>.Empty;
90+
public virtual IReadOnlyCollection<ISpan> Spans => ImmutableList<ISpan>.Empty;
9191

9292
public IReadOnlyCollection<Breadcrumb> Breadcrumbs => ImmutableList<Breadcrumb>.Empty;
9393

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
using Sentry.Extensibility;
2+
3+
namespace Sentry.Internal;
4+
5+
/// <summary>
6+
/// We know already, when starting a transaction, whether it's going to be sampled or not. When it's not sampled, we can
7+
/// avoid lots of unecessary processing. The only thing we need to track is the number of spans that would have been
8+
/// created (the client reports detailing discarded events includes this detail).
9+
/// </summary>
10+
internal sealed class UnsampledTransaction : NoOpTransaction
11+
{
12+
// Although it's a little bit wasteful to create separate individual class instances here when all we're going to
13+
// report to sentry is the span count (in the client report), SDK users may refer to things like
14+
// `ITransaction.Spans.Count`, so we create an actual collection
15+
private readonly ConcurrentBag<ISpan> _spans = [];
16+
private readonly IHub _hub;
17+
private readonly ITransactionContext _context;
18+
private readonly SentryOptions? _options;
19+
20+
public UnsampledTransaction(IHub hub, ITransactionContext context)
21+
{
22+
_hub = hub;
23+
_options = _hub.GetSentryOptions();
24+
_options?.LogDebug("Starting unsampled transaction");
25+
_context = context;
26+
}
27+
28+
internal DynamicSamplingContext? DynamicSamplingContext { get; set; }
29+
30+
public override IReadOnlyCollection<ISpan> Spans => _spans;
31+
32+
public override SpanId SpanId => _context.SpanId;
33+
public override SentryId TraceId => _context.TraceId;
34+
public override bool? IsSampled => false;
35+
36+
public override string Name
37+
{
38+
get => _context.Name;
39+
set { }
40+
}
41+
42+
public override string Operation
43+
{
44+
get => _context.Operation;
45+
set { }
46+
}
47+
48+
public override void Finish()
49+
{
50+
_options?.LogDebug("Finishing unsampled transaction");
51+
52+
// Clear the transaction from the scope
53+
_hub.ConfigureScope(scope => scope.ResetTransaction(this));
54+
55+
// Record the discarded events
56+
var spanCount = Spans.Count + 1; // 1 for each span + 1 for the transaction itself
57+
_options?.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.SampleRate, DataCategory.Transaction);
58+
_options?.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.SampleRate, DataCategory.Span, spanCount);
59+
60+
_options?.LogDebug("Finished unsampled transaction");
61+
}
62+
63+
public override void Finish(SpanStatus status) => Finish();
64+
65+
public override void Finish(Exception exception, SpanStatus status) => Finish();
66+
67+
public override void Finish(Exception exception) => Finish();
68+
69+
/// <inheritdoc />
70+
public override SentryTraceHeader GetTraceHeader() => new(TraceId, SpanId, IsSampled);
71+
72+
public override ISpan StartChild(string operation)
73+
{
74+
var span = new UnsampledSpan(this);
75+
_spans.Add(span);
76+
return span;
77+
}
78+
79+
private class UnsampledSpan(UnsampledTransaction transaction) : NoOpSpan
80+
{
81+
public override ISpan StartChild(string operation) => transaction.StartChild(operation);
82+
}
83+
}

0 commit comments

Comments
 (0)