Skip to content

Commit ebac4e3

Browse files
fix: InvalidCastException in SentrySpanProcessor (#4245)
Resolves #4235: - #4235
1 parent 9beef00 commit ebac4e3

File tree

6 files changed

+172
-32
lines changed

6 files changed

+172
-32
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
### Fixes
1010

11+
- InvalidCastException in SentrySpanProcessor when using the Sentry.OpenTelemetry integration ([#4245](https://github.com/getsentry/sentry-dotnet/pull/4245))
1112
- Fix InApp Exclude for frames without Module by checking against frame's Package ([#4236](https://github.com/getsentry/sentry-dotnet/pull/4236))
1213

1314
## 5.9.0

src/Sentry.OpenTelemetry/SentrySpanProcessor.cs

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -133,12 +133,15 @@ private void CreateChildSpan(Activity data, ISpan parentSpan, SpanId? parentSpan
133133
Instrumenter = Instrumenter.OpenTelemetry
134134
};
135135

136-
var span = (SpanTracer)parentSpan.StartChild(context);
137-
span.Origin = OpenTelemetryOrigin;
138-
span.StartTimestamp = data.StartTimeUtc;
139-
// Used to filter out spans that are not recorded when finishing a transaction.
140-
span.SetFused(data);
141-
span.IsFiltered = () => span.GetFused<Activity>() is { IsAllDataRequested: false, Recorded: false };
136+
var span = parentSpan.StartChild(context);
137+
if (span is SpanTracer spanTracer)
138+
{
139+
spanTracer.Origin = OpenTelemetryOrigin;
140+
spanTracer.StartTimestamp = data.StartTimeUtc;
141+
// Used to filter out spans that are not recorded when finishing a transaction.
142+
spanTracer.SetFused(data);
143+
spanTracer.IsFiltered = () => spanTracer.GetFused<Activity>() is { IsAllDataRequested: false, Recorded: false };
144+
}
142145
_map[data.SpanId] = span;
143146
}
144147

@@ -161,17 +164,19 @@ private void CreateRootSpan(Activity data)
161164

162165
var baggageHeader = data.Baggage.AsBaggageHeader();
163166
var dynamicSamplingContext = baggageHeader.CreateDynamicSamplingContext(_replaySession);
164-
var transaction = (TransactionTracer)_hub.StartTransaction(
167+
var transaction = _hub.StartTransaction(
165168
transactionContext, new Dictionary<string, object?>(), dynamicSamplingContext
166169
);
167-
transaction.Contexts.Trace.Origin = OpenTelemetryOrigin;
168-
transaction.StartTimestamp = data.StartTimeUtc;
170+
if (transaction is TransactionTracer tracer)
171+
{
172+
tracer.Contexts.Trace.Origin = OpenTelemetryOrigin;
173+
tracer.StartTimestamp = data.StartTimeUtc;
174+
}
169175
_hub.ConfigureScope(scope => scope.Transaction = transaction);
170176
transaction.SetFused(data);
171177
_map[data.SpanId] = transaction;
172178
}
173179

174-
175180
/// <inheritdoc />
176181
public override void OnEnd(Activity data)
177182
{
@@ -236,15 +241,15 @@ public override void OnEnd(Activity data)
236241
// Transactions set otel attributes (and resource attributes) as context.
237242
transaction.Contexts["otel"] = GetOtelContext(attributes);
238243
}
239-
else
244+
else if (span is SpanTracer spanTracer)
240245
{
241246
// Use the end timestamp from the activity data.
242-
((SpanTracer)span).EndTimestamp = data.StartTimeUtc + data.Duration;
247+
spanTracer.EndTimestamp = data.StartTimeUtc + data.Duration;
243248

244249
// Spans set otel attributes in extras (passed to Sentry as "data" on the span).
245250
// Resource attributes do not need to be set, as they would be identical as those set on the transaction.
246-
span.SetExtras(attributes);
247-
span.SetExtra("otel.kind", data.Kind);
251+
spanTracer.SetExtras(attributes);
252+
spanTracer.SetExtra("otel.kind", data.Kind);
248253
}
249254

250255
// In ASP.NET Core the middleware finishes up (and the scope gets popped) before the activity is ended. So we

src/Sentry/ISpan.cs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,21 @@ public static ISpan StartChild(this ISpan span, string operation, string? descri
7070

7171
internal static ISpan StartChild(this ISpan span, SpanContext context)
7272
{
73-
var transaction = span.GetTransaction() as TransactionTracer;
74-
if (transaction?.StartChild(context.SpanId, span.SpanId, context.Operation, context.Instrumenter)
75-
is not SpanTracer childSpan)
73+
var transaction = span.GetTransaction();
74+
if (transaction is TransactionTracer transactionTracer)
7675
{
77-
return NoOpSpan.Instance;
76+
var child = transactionTracer.StartChild(context.SpanId, span.SpanId, context.Operation, context.Instrumenter);
77+
if (child is SpanTracer childTracer)
78+
{
79+
childTracer.Description = context.Description;
80+
return childTracer;
81+
}
7882
}
79-
80-
childSpan.Description = context.Description;
81-
return childSpan;
83+
if (transaction is UnsampledTransaction unsampledTransaction)
84+
{
85+
return unsampledTransaction.StartChild(context.Operation, context.SpanId);
86+
}
87+
return NoOpSpan.Instance;
8288
}
8389

8490
/// <summary>
@@ -88,7 +94,8 @@ public static ITransactionTracer GetTransaction(this ISpan span) =>
8894
span switch
8995
{
9096
ITransactionTracer transaction => transaction,
91-
SpanTracer tracer => tracer.Transaction,
97+
UnsampledSpan unsampledSpan => unsampledSpan.Transaction,
98+
SpanTracer spanTracer => spanTracer.Transaction,
9299
_ => throw new ArgumentOutOfRangeException(nameof(span), span, null)
93100
};
94101

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Sentry.Internal;
2+
3+
internal sealed class UnsampledSpan(UnsampledTransaction transaction, SpanId? spanId = null) : NoOpSpan
4+
{
5+
public override bool? IsSampled => false;
6+
public override SpanId SpanId { get; } = spanId ?? SpanId.Empty;
7+
internal UnsampledTransaction Transaction => transaction;
8+
public override ISpan StartChild(string operation) => transaction.StartChild(operation);
9+
}

src/Sentry/Internal/UnsampledTransaction.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,10 @@ public override ISpan StartChild(string operation)
9494
return span;
9595
}
9696

97-
private class UnsampledSpan(UnsampledTransaction transaction) : NoOpSpan
97+
public ISpan StartChild(string operation, SpanId spanId)
9898
{
99-
public override ISpan StartChild(string operation) => transaction.StartChild(operation);
100-
101-
public override bool? IsSampled => false;
99+
var span = new UnsampledSpan(this, spanId);
100+
_spans.Add(span);
101+
return span;
102102
}
103103
}

test/Sentry.OpenTelemetry.Tests/SentrySpanProcessorTests.cs

Lines changed: 124 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,11 @@ public void OnStart_Transaction_With_DynamicSamplingContext()
146146
}
147147

148148
[Fact]
149-
public void OnStart_WithParentSpanId_StartsChildSpan()
149+
public void OnStart_SampledWithParentSpanId_StartsChildSpan()
150150
{
151151
// Arrange
152152
_fixture.Options.Instrumenter = Instrumenter.OpenTelemetry;
153+
_fixture.Options.TracesSampleRate = 1.0;
153154
var sut = _fixture.GetSut();
154155

155156
using var parent = Tracer.StartActivity("Parent");
@@ -164,16 +165,18 @@ public void OnStart_WithParentSpanId_StartsChildSpan()
164165
Assert.True(sut._map.TryGetValue(data.SpanId, out var span));
165166
using (new AssertionScope())
166167
{
167-
span.Should().BeOfType<SpanTracer>();
168+
span.IsSampled.Should().Be(true);
168169
span.SpanId.Should().Be(data.SpanId.AsSentrySpanId());
170+
169171
if (span is not SpanTracer spanTracer)
170172
{
171173
Assert.Fail("Span is not a span tracer");
172174
return;
173175
}
176+
177+
span.SpanId.Should().Be(data.SpanId.AsSentrySpanId());
174178
using (new AssertionScope())
175179
{
176-
spanTracer.SpanId.Should().Be(data.SpanId.AsSentrySpanId());
177180
spanTracer.ParentSpanId.Should().Be(data.ParentSpanId.AsSentrySpanId());
178181
spanTracer.TraceId.Should().Be(data.TraceId.AsSentryId());
179182
spanTracer.Operation.Should().Be(data.OperationName);
@@ -184,6 +187,32 @@ public void OnStart_WithParentSpanId_StartsChildSpan()
184187
}
185188
}
186189

190+
[Fact]
191+
public void OnStart_NotSampledWithParentSpanId_StartsChildSpan()
192+
{
193+
// Arrange
194+
_fixture.Options.Instrumenter = Instrumenter.OpenTelemetry;
195+
_fixture.Options.TracesSampleRate = 0.0;
196+
var sut = _fixture.GetSut();
197+
198+
using var parent = Tracer.StartActivity("Parent");
199+
sut.OnStart(parent);
200+
201+
using var data = Tracer.StartActivity("TestActivity");
202+
203+
// Act
204+
sut.OnStart(data!);
205+
206+
// Assert
207+
Assert.True(sut._map.TryGetValue(data.SpanId, out var span));
208+
using (new AssertionScope())
209+
{
210+
span.IsSampled.Should().Be(false);
211+
span.SpanId.Should().Be(data.SpanId.AsSentrySpanId());
212+
span.Should().BeOfType<UnsampledSpan>();
213+
}
214+
}
215+
187216
[Fact]
188217
public void OnStart_WithSentryParentSpanId_StartsChildSpan()
189218
{
@@ -257,10 +286,11 @@ public void StartSpan_UsingSentryTracing_StartsChildSpan()
257286
}
258287

259288
[Fact]
260-
public void OnStart_WithoutParentSpanId_StartsNewTransaction()
289+
public void OnStart_SampledWithoutParentSpanId_StartsNewTransaction()
261290
{
262291
// Arrange
263292
_fixture.Options.Instrumenter = Instrumenter.OpenTelemetry;
293+
_fixture.Options.TracesSampleRate = 1.0;
264294
_fixture.ScopeManager = Substitute.For<IInternalScopeManager>();
265295
var scope = new Scope();
266296
var clientScope = new KeyValuePair<Scope, ISentryClient>(scope, _fixture.Client);
@@ -279,6 +309,7 @@ public void OnStart_WithoutParentSpanId_StartsNewTransaction()
279309
Assert.Fail("Span is not a transaction tracer");
280310
return;
281311
}
312+
282313
using (new AssertionScope())
283314
{
284315
transaction.SpanId.Should().Be(data.SpanId.AsSentrySpanId());
@@ -293,7 +324,39 @@ public void OnStart_WithoutParentSpanId_StartsNewTransaction()
293324
}
294325

295326
[Fact]
296-
public void OnEnd_FinishesSpan()
327+
public void OnStart_NotSampledWithoutParentSpanId_StartsNewTransaction()
328+
{
329+
// Arrange
330+
_fixture.Options.Instrumenter = Instrumenter.OpenTelemetry;
331+
_fixture.Options.TracesSampleRate = 0.0;
332+
_fixture.ScopeManager = Substitute.For<IInternalScopeManager>();
333+
var scope = new Scope();
334+
var clientScope = new KeyValuePair<Scope, ISentryClient>(scope, _fixture.Client);
335+
_fixture.ScopeManager.GetCurrent().Returns(clientScope);
336+
var sut = _fixture.GetSut();
337+
338+
var data = Tracer.StartActivity("test op");
339+
340+
// Act
341+
sut.OnStart(data!);
342+
343+
// Assert
344+
Assert.True(sut._map.TryGetValue(data.SpanId, out var span));
345+
if (span is not UnsampledTransaction transaction)
346+
{
347+
Assert.Fail("Span is not an unsampled transaction");
348+
return;
349+
}
350+
351+
using (new AssertionScope())
352+
{
353+
transaction.SpanId.Should().Be(data.SpanId.AsSentrySpanId());
354+
transaction.TraceId.Should().Be(data.TraceId.AsSentryId());
355+
}
356+
}
357+
358+
[Fact]
359+
public void OnEnd_Sampled_Span_FinishesSpan()
297360
{
298361
// Arrange
299362
_fixture.Options.Instrumenter = Instrumenter.OpenTelemetry;
@@ -338,6 +401,34 @@ public void OnEnd_FinishesSpan()
338401
}
339402
}
340403

404+
[Fact]
405+
public void OnEnd_Unsampled_Span_DoesNotThrow()
406+
{
407+
// Arrange
408+
_fixture.Options.Instrumenter = Instrumenter.OpenTelemetry;
409+
_fixture.Options.TracesSampleRate = 0.0;
410+
var sut = _fixture.GetSut();
411+
412+
var parent = Tracer.StartActivity(name: "transaction")!;
413+
sut.OnStart(parent);
414+
415+
Dictionary<string, object> tags = [];
416+
var data = Tracer.StartActivity(name: "test operation", kind: ActivityKind.Internal, parentContext: default, tags)!;
417+
data.DisplayName = "test display name";
418+
sut.OnStart(data);
419+
420+
sut._map.TryGetValue(data.SpanId, out var span);
421+
422+
// Act
423+
sut.OnEnd(data);
424+
425+
// Assert
426+
span.Should().BeOfType<UnsampledSpan>();
427+
428+
// There's nothing else to assert here, as long as calling OnEnd does not throw an exception,
429+
// UnsampledSpan.Finish() is basically a no-op.
430+
}
431+
341432
[Fact]
342433
public void OnEnd_Transaction_RestoresSavedScope()
343434
{
@@ -418,7 +509,7 @@ public void OnEnd_SpansEnriched()
418509
}
419510

420511
[Fact]
421-
public void OnEnd_FinishesTransaction()
512+
public void OnEnd_Sampled_FinishesTransaction()
422513
{
423514
// Arrange
424515
_fixture.Options.Instrumenter = Instrumenter.OpenTelemetry;
@@ -460,6 +551,33 @@ public void OnEnd_FinishesTransaction()
460551
}
461552
}
462553

554+
[Fact]
555+
public void OnEnd_NotSampled_FinishesTransaction()
556+
{
557+
// Arrange
558+
_fixture.Options.Instrumenter = Instrumenter.OpenTelemetry;
559+
_fixture.Options.TracesSampleRate = 0.0;
560+
var sut = _fixture.GetSut();
561+
562+
Dictionary<string, object> tags = [];
563+
var data = Tracer.StartActivity(name: "test operation", kind: ActivityKind.Internal, parentContext: default, tags)!;
564+
data.DisplayName = "test display name";
565+
sut.OnStart(data);
566+
567+
sut._map.TryGetValue(data.SpanId, out var span);
568+
569+
// Act
570+
sut.OnEnd(data);
571+
572+
// Assert
573+
if (span is not UnsampledTransaction transaction)
574+
{
575+
Assert.Fail("Span is not an unsampled transaction");
576+
return;
577+
}
578+
transaction.IsFinished.Should().BeTrue();
579+
}
580+
463581
[Fact]
464582
public void OnEnd_FilteredTransaction_DoesNotFinishTransaction()
465583
{

0 commit comments

Comments
 (0)