Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public SpanId? ParentSpanId
}
}

public bool IsSampled => Current?.Recorded ?? false;
public bool? IsSampled => Current?.Recorded;

/// <summary>
/// th is a rejection threshold: T = (1 - sampling_probability) * 2^56, so we invert to get the sample rate.
Expand Down
24 changes: 24 additions & 0 deletions src/Sentry/Internal/Hub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,16 @@ internal ITransactionTracer StartTransaction(
return NoOpTransaction.Instance;
}

if (context.ParentSpanId is null
&& _options.ExternalPropagationContext?.Snapshot() is { TraceId: { } externalTraceId } externalPropagationContext)
{
context = ContinueExternalTrace(context, externalTraceId, externalPropagationContext);
if (externalPropagationContext.IsSampled is not null)
{
dynamicSamplingContext ??= externalPropagationContext.GetDynamicSamplingContext(_options, _replaySession);
}
}

bool? isSampled = null;
double? sampleRate = null;
DiscardReason? discardReason = null;
Expand Down Expand Up @@ -295,6 +305,20 @@ internal ITransactionTracer StartTransaction(
return transaction;
}

// Without a sampling decision to inherit, the transaction is a root in the external trace and samples itself.
private static TransactionContext ContinueExternalTrace(ITransactionContext context, SentryId traceId,
IExternalPropagationContext externalPropagationContext)
{
var isSampled = externalPropagationContext.IsSampled;
var parentSpanId = isSampled is null ? null : externalPropagationContext.SpanId;
return new TransactionContext(context.Name, context.Operation, context.SpanId, parentSpanId, traceId,
context.Description, context.Status, isSampled ?? context.IsSampled, isSampled, context.NameSource)
{
Instrumenter = (context as SpanContext)?.Instrumenter ?? Instrumenter.Sentry,
Origin = context.Origin
};
}

public void BindException(Exception exception, ISpan span)
{
// Don't bind on sampled out spans
Expand Down
2 changes: 1 addition & 1 deletion src/Sentry/Internal/IExternalPropagationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ internal interface IExternalPropagationContext
public SentryId? TraceId { get; }
public SpanId? SpanId { get; }
public SpanId? ParentSpanId { get; }
public bool IsSampled { get; }
public bool? IsSampled { get; }
public double? SampleRate { get; }
public double? SampleRand { get; }

Expand Down
106 changes: 106 additions & 0 deletions test/Sentry.Tests/HubTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1894,6 +1894,112 @@ public void CaptureEvent_ExternalPropagationContext_PrefersExternalContext()
evt.DynamicSamplingContext.Should().Be(dynamicSamplingContext);
}

[Fact]
public void GetTraceHeader_ExternalPropagationContextWithoutSamplingDecision_OmitsDecision()
{
// Arrange
var externalContext = Substitute.For<IExternalPropagationContext>();
externalContext.Snapshot().Returns(externalContext);
externalContext.TraceId.Returns(SentryId.Parse("75302ac48a024bde9a3b3734a82e36c8"));
externalContext.SpanId.Returns(SpanId.Parse("2000000000000000"));
externalContext.IsSampled.Returns((bool?)null);
_fixture.Options.ExternalPropagationContext = externalContext;
var hub = _fixture.GetSut();

// Act
var header = hub.GetTraceHeader();

// Assert
header.IsSampled.Should().BeNull();
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public void StartTransaction_ExternalPropagationContextWithSamplingDecision_ContinuesExternalSpan(bool isSampled)
{
// Arrange
var traceId = SentryId.Parse("75302ac48a024bde9a3b3734a82e36c8");
var spanId = SpanId.Parse("2000000000000000");
var dynamicSamplingContext = BaggageHeader.Create(new List<KeyValuePair<string, string>>
{
{"sentry-trace_id", "75302ac48a024bde9a3b3734a82e36c8"},
{"sentry-public_key", "49d0f7386ad645858ae85020e393bef3"},
{"sentry-sample_rate", "1.0"},
{"sentry-sample_rand", "0.5"}
}).CreateDynamicSamplingContext(_fixture.ReplaySession);
var externalContext = Substitute.For<IExternalPropagationContext>();
externalContext.Snapshot().Returns(externalContext);
externalContext.TraceId.Returns(traceId);
externalContext.SpanId.Returns(spanId);
externalContext.IsSampled.Returns(isSampled);
externalContext.GetDynamicSamplingContext(Arg.Any<SentryOptions>(), Arg.Any<IReplaySession>())
.Returns(dynamicSamplingContext);
_fixture.Options.ExternalPropagationContext = externalContext;
_fixture.Options.TracesSampleRate = isSampled ? 0.0 : 1.0;
var hub = _fixture.GetSut();

// Act
var transaction = hub.StartTransaction("name", "operation");

// Assert
transaction.TraceId.Should().Be(traceId);
transaction.IsSampled.Should().Be(isSampled);
if (isSampled)
{
transaction.ParentSpanId.Should().Be(spanId);
}
transaction.GetDynamicSamplingContext().Should().Be(dynamicSamplingContext);
}

[Fact]
public void StartTransaction_ExternalPropagationContextWithoutSamplingDecision_IsRootInExternalTrace()
{
// Arrange
var traceId = SentryId.Parse("75302ac48a024bde9a3b3734a82e36c8");
var externalContext = Substitute.For<IExternalPropagationContext>();
externalContext.Snapshot().Returns(externalContext);
externalContext.TraceId.Returns(traceId);
externalContext.SpanId.Returns(SpanId.Parse("2000000000000000"));
externalContext.IsSampled.Returns((bool?)null);
_fixture.Options.ExternalPropagationContext = externalContext;
_fixture.Options.TracesSampleRate = 1.0;
var hub = _fixture.GetSut();

// Act
var transaction = hub.StartTransaction("name", "operation");

// Assert
transaction.TraceId.Should().Be(traceId);
transaction.ParentSpanId.Should().BeNull();
transaction.IsSampled.Should().BeTrue();
transaction.GetDynamicSamplingContext()!.Items["trace_id"].Should().Be(traceId.ToString());
}

[Fact]
public void StartTransaction_ContextWithParent_IgnoresExternalPropagationContext()
{
// Arrange
var externalContext = Substitute.For<IExternalPropagationContext>();
externalContext.Snapshot().Returns(externalContext);
externalContext.TraceId.Returns(SentryId.Parse("75302ac48a024bde9a3b3734a82e36c8"));
externalContext.SpanId.Returns(SpanId.Parse("2000000000000000"));
externalContext.IsSampled.Returns(true);
_fixture.Options.ExternalPropagationContext = externalContext;
_fixture.Options.TracesSampleRate = 1.0;
var hub = _fixture.GetSut();
var traceId = SentryId.Parse("5bd5f6d346b442dd9177dce9302fd737");
var parentSpanId = SpanId.Parse("3000000000000000");
var context = new TransactionContext("name", "operation", parentSpanId: parentSpanId, traceId: traceId);

// Act
var transaction = hub.StartTransaction(context);

// Assert
transaction.TraceId.Should().Be(traceId);
transaction.ParentSpanId.Should().Be(parentSpanId);
}

[Fact]
public void ContinueTrace_ReceivesHeaders_SetsPropagationContextAndReturnsTransactionContext()
{
Expand Down
Loading