diff --git a/CHANGELOG.md b/CHANGELOG.md
index b40ed69f63..55cdefbcea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
## vNext
- Add support for dynamic transaction sampling. (#753) @Tyrrrz
+- Integrate trace headers. (#758) @Tyrrrz
- Renamed Option `DiagnosticsLevel` to `DiagnosticLevel` (#759) @bruno-garcia
## 3.0.0-beta.0
diff --git a/src/Sentry/Extensibility/DisabledHub.cs b/src/Sentry/Extensibility/DisabledHub.cs
index 4e9cbf922a..f870979c43 100644
--- a/src/Sentry/Extensibility/DisabledHub.cs
+++ b/src/Sentry/Extensibility/DisabledHub.cs
@@ -65,7 +65,7 @@ public ITransaction StartTransaction(
///
/// Returns null.
///
- public SentryTraceHeader? GetSentryTrace() => null;
+ public SentryTraceHeader? GetTraceHeader() => null;
///
/// No-Op.
@@ -82,7 +82,7 @@ public void BindClient(ISentryClient client)
///
/// No-Op.
///
- public void CaptureTransaction(Transaction transaction)
+ public void CaptureTransaction(ITransaction transaction)
{
}
diff --git a/src/Sentry/Extensibility/HubAdapter.cs b/src/Sentry/Extensibility/HubAdapter.cs
index dad8d90278..ab3a0075d0 100644
--- a/src/Sentry/Extensibility/HubAdapter.cs
+++ b/src/Sentry/Extensibility/HubAdapter.cs
@@ -84,7 +84,7 @@ public ITransaction StartTransaction(
/// Forwards the call to .
///
[DebuggerStepThrough]
- public SentryTraceHeader? GetSentryTrace()
+ public SentryTraceHeader? GetTraceHeader()
=> SentrySdk.GetTraceHeader();
///
@@ -153,7 +153,7 @@ public SentryId CaptureEvent(SentryEvent evt, Scope? scope)
///
[DebuggerStepThrough]
[EditorBrowsable(EditorBrowsableState.Never)]
- public void CaptureTransaction(Transaction transaction)
+ public void CaptureTransaction(ITransaction transaction)
=> SentrySdk.CaptureTransaction(transaction);
///
diff --git a/src/Sentry/HubExtensions.cs b/src/Sentry/HubExtensions.cs
index 4670db541e..2f1a8802c2 100644
--- a/src/Sentry/HubExtensions.cs
+++ b/src/Sentry/HubExtensions.cs
@@ -42,6 +42,27 @@ public static ITransaction StartTransaction(
return transaction;
}
+ ///
+ /// Starts a transaction from the specified trace header.
+ ///
+ public static ITransaction StartTransaction(
+ this IHub hub,
+ string name,
+ string operation,
+ SentryTraceHeader traceHeader)
+ {
+ var context = new TransactionContext(
+ // SpanId from the header becomes ParentSpanId on this transaction
+ traceHeader.SpanId,
+ traceHeader.TraceId,
+ name,
+ operation,
+ traceHeader.IsSampled
+ );
+
+ return hub.StartTransaction(context);
+ }
+
///
/// Adds a breadcrumb to the current scope.
///
diff --git a/src/Sentry/IHub.cs b/src/Sentry/IHub.cs
index 6bfb0c5887..4caa331e4f 100644
--- a/src/Sentry/IHub.cs
+++ b/src/Sentry/IHub.cs
@@ -31,8 +31,8 @@ ITransaction StartTransaction(
);
///
- /// Gets the sentry trace header.
+ /// Gets the Sentry trace header for the last active span.
///
- SentryTraceHeader? GetSentryTrace();
+ SentryTraceHeader? GetTraceHeader();
}
}
diff --git a/src/Sentry/ISentryClient.cs b/src/Sentry/ISentryClient.cs
index 47e412e479..c8f68f9f46 100644
--- a/src/Sentry/ISentryClient.cs
+++ b/src/Sentry/ISentryClient.cs
@@ -31,8 +31,12 @@ public interface ISentryClient
///
/// Captures a transaction.
///
+ ///
+ /// Note: this method is NOT meant to be called from user code!
+ /// Instead, call on the transaction.
+ ///
/// The transaction.
- void CaptureTransaction(Transaction transaction);
+ void CaptureTransaction(ITransaction transaction);
///
/// Flushes events queued up.
diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs
index 224a1a50da..18b86057c9 100644
--- a/src/Sentry/Internal/Hub.cs
+++ b/src/Sentry/Internal/Hub.cs
@@ -156,10 +156,10 @@ public ITransaction StartTransaction(
return transaction;
}
- public SentryTraceHeader? GetSentryTrace()
+ public SentryTraceHeader? GetTraceHeader()
{
var (currentScope, _) = ScopeManager.GetCurrent();
- return currentScope.Transaction?.GetTraceHeader();
+ return currentScope.GetSpan()?.GetTraceHeader();
}
public SentryId CaptureEvent(SentryEvent evt, Scope? scope = null)
@@ -191,11 +191,20 @@ public void CaptureUserFeedback(UserFeedback userFeedback)
}
}
- public void CaptureTransaction(Transaction transaction)
+ public void CaptureTransaction(ITransaction transaction)
{
try
{
_ownedClient.CaptureTransaction(transaction);
+
+ // Clear the transaction from the scope
+ ScopeManager.WithScope(scope =>
+ {
+ if (scope.Transaction == transaction)
+ {
+ scope.Transaction = null;
+ }
+ });
}
catch (Exception e)
{
diff --git a/src/Sentry/Protocol/Envelopes/Envelope.cs b/src/Sentry/Protocol/Envelopes/Envelope.cs
index b500fa4c7e..c319d1f38d 100644
--- a/src/Sentry/Protocol/Envelopes/Envelope.cs
+++ b/src/Sentry/Protocol/Envelopes/Envelope.cs
@@ -115,7 +115,7 @@ public static Envelope FromUserFeedback(UserFeedback sentryUserFeedback)
///
/// Creates an envelope that contains a single transaction.
///
- public static Envelope FromTransaction(Transaction transaction)
+ public static Envelope FromTransaction(ITransaction transaction)
{
var header = new Dictionary(StringComparer.Ordinal)
{
diff --git a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs
index 9b63004875..e300f44b52 100644
--- a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs
+++ b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs
@@ -144,14 +144,14 @@ public static EnvelopeItem FromUserFeedback(UserFeedback sentryUserFeedback)
///
/// Creates an envelope item from transaction.
///
- public static EnvelopeItem FromTransaction(Transaction transaction)
+ public static EnvelopeItem FromTransaction(ITransaction transaction)
{
var header = new Dictionary(StringComparer.Ordinal)
{
[TypeKey] = TypeValueTransaction
};
- return new EnvelopeItem(header, new JsonSerializable(transaction));
+ return new EnvelopeItem(header, new JsonSerializable((IJsonSerializable)transaction));
}
///
diff --git a/src/Sentry/Protocol/IJsonSerializable.cs b/src/Sentry/Protocol/IJsonSerializable.cs
index e3ac8f3f61..1ab4fafcdf 100644
--- a/src/Sentry/Protocol/IJsonSerializable.cs
+++ b/src/Sentry/Protocol/IJsonSerializable.cs
@@ -13,6 +13,10 @@ internal interface IJsonSerializable
///
/// Writes the object as JSON.
///
+ ///
+ /// Note: this method is meant only for internal use and is exposed due to a language limitation.
+ /// Avoid relying on this method in user code.
+ ///
void WriteTo(Utf8JsonWriter writer);
}
diff --git a/src/Sentry/Protocol/ISpan.cs b/src/Sentry/Protocol/ISpan.cs
index db7be3f1e9..f182b48ea1 100644
--- a/src/Sentry/Protocol/ISpan.cs
+++ b/src/Sentry/Protocol/ISpan.cs
@@ -35,6 +35,11 @@ public interface ISpan : ISpanContext, IHasTags, IHasExtra
///
DateTimeOffset? EndTimestamp { get; }
+ ///
+ /// Whether the span is finished.
+ ///
+ bool IsFinished { get; }
+
///
/// Starts a child span.
///
@@ -44,6 +49,11 @@ public interface ISpan : ISpanContext, IHasTags, IHasExtra
/// Finishes the span.
///
void Finish();
+
+ ///
+ /// Get Sentry trace header.
+ ///
+ SentryTraceHeader GetTraceHeader();
}
///
diff --git a/src/Sentry/Protocol/ITransaction.cs b/src/Sentry/Protocol/ITransaction.cs
index 29b2bd9ed6..65e34d0c3d 100644
--- a/src/Sentry/Protocol/ITransaction.cs
+++ b/src/Sentry/Protocol/ITransaction.cs
@@ -24,8 +24,8 @@ public interface ITransaction : ISpan, ITransactionContext, IEventLike
IReadOnlyCollection Spans { get; }
///
- /// Get Sentry trace header.
+ /// Gets the last active (not finished) span in this transaction.
///
- SentryTraceHeader GetTraceHeader();
+ ISpan? GetLastActiveSpan();
}
}
diff --git a/src/Sentry/Protocol/SentryTraceHeader.cs b/src/Sentry/Protocol/SentryTraceHeader.cs
index e08d4aaaec..85fc12ee34 100644
--- a/src/Sentry/Protocol/SentryTraceHeader.cs
+++ b/src/Sentry/Protocol/SentryTraceHeader.cs
@@ -7,24 +7,37 @@ namespace Sentry.Protocol
///
public class SentryTraceHeader
{
- private readonly SentryId _traceId;
- private readonly SpanId _spanId;
- private readonly bool? _isSampled;
+ internal const string HttpHeaderName = "sentry-trace";
+
+ ///
+ /// Trace ID.
+ ///
+ public SentryId TraceId { get; }
+
+ ///
+ /// Span ID.
+ ///
+ public SpanId SpanId { get; }
+
+ ///
+ /// Whether the trace is sampled.
+ ///
+ public bool? IsSampled { get; }
///
/// Initializes an instance of .
///
- public SentryTraceHeader(SentryId traceId, SpanId spanId, bool? isSampled)
+ public SentryTraceHeader(SentryId traceId, SpanId spanSpanId, bool? isSampled)
{
- _traceId = traceId;
- _spanId = spanId;
- _isSampled = isSampled;
+ TraceId = traceId;
+ SpanId = spanSpanId;
+ IsSampled = isSampled;
}
///
- public override string ToString() => _isSampled is {} isSampled
- ? $"{_traceId}-{_spanId}-{(isSampled ? 1 : 0)}"
- : $"{_traceId}-{_spanId}";
+ public override string ToString() => IsSampled is {} isSampled
+ ? $"{TraceId}-{SpanId}-{(isSampled ? 1 : 0)}"
+ : $"{TraceId}-{SpanId}";
///
/// Parses from string.
diff --git a/src/Sentry/Protocol/Span.cs b/src/Sentry/Protocol/Span.cs
index 02271c0358..4a45a9675c 100644
--- a/src/Sentry/Protocol/Span.cs
+++ b/src/Sentry/Protocol/Span.cs
@@ -33,6 +33,9 @@ public class Span : ISpan, IJsonSerializable
///
public DateTimeOffset? EndTimestamp { get; private set; }
+ ///
+ public bool IsFinished => EndTimestamp is not null;
+
///
public string Operation { get; set; }
@@ -86,6 +89,13 @@ public ISpan StartChild(string operation) =>
///
public void Finish() => EndTimestamp = DateTimeOffset.UtcNow;
+ ///
+ public SentryTraceHeader GetTraceHeader() => new(
+ TraceId,
+ SpanId,
+ IsSampled
+ );
+
///
public void WriteTo(Utf8JsonWriter writer)
{
diff --git a/src/Sentry/Protocol/Transaction.cs b/src/Sentry/Protocol/Transaction.cs
index 3d05d18181..5f8fb4e2af 100644
--- a/src/Sentry/Protocol/Transaction.cs
+++ b/src/Sentry/Protocol/Transaction.cs
@@ -65,10 +65,10 @@ public SentryId TraceId
public string Name { get; set; }
///
- public DateTimeOffset StartTimestamp { get; private set; } = DateTimeOffset.UtcNow;
+ public DateTimeOffset StartTimestamp { get; internal set; } = DateTimeOffset.UtcNow;
///
- public DateTimeOffset? EndTimestamp { get; private set; }
+ public DateTimeOffset? EndTimestamp { get; internal set; }
///
public string Operation
@@ -170,6 +170,9 @@ public IReadOnlyList Fingerprint
///
public IReadOnlyCollection Spans => _spansLazy.Value;
+ ///
+ public bool IsFinished => EndTimestamp is not null;
+
// This constructor is used for deserialization purposes.
// It's required because some of the fields are mapped on 'contexts.trace'.
// When deserializing, we don't parse those fields explicitly, but
@@ -251,6 +254,9 @@ public void Finish()
_client.CaptureTransaction(this);
}
+ ///
+ public ISpan? GetLastActiveSpan() => Spans.LastOrDefault(s => !s.IsFinished);
+
///
public SentryTraceHeader GetTraceHeader() => new(
TraceId,
diff --git a/src/Sentry/Protocol/TransactionContext.cs b/src/Sentry/Protocol/TransactionContext.cs
index 8221359955..63a2e7b43d 100644
--- a/src/Sentry/Protocol/TransactionContext.cs
+++ b/src/Sentry/Protocol/TransactionContext.cs
@@ -29,11 +29,12 @@ public TransactionContext(
/// Initializes an instance of .
///
public TransactionContext(
+ SpanId? parentSpanId,
+ SentryId traceId,
string name,
string operation,
- string description,
bool? isSampled)
- : this(SpanId.Create(), null, SentryId.Create(), name, operation, description, null, isSampled)
+ : this(SpanId.Create(), parentSpanId, traceId, name, operation, "", null, isSampled)
{
}
@@ -44,7 +45,7 @@ public TransactionContext(
string name,
string operation,
bool? isSampled)
- : this(name, operation, "", isSampled)
+ : this(null, SentryId.Create(), name, operation, isSampled)
{
}
diff --git a/src/Sentry/Scope.cs b/src/Sentry/Scope.cs
index 0b4c3a23da..75c5a68193 100644
--- a/src/Sentry/Scope.cs
+++ b/src/Sentry/Scope.cs
@@ -382,5 +382,11 @@ internal void Evaluate()
}
}
}
+
+ ///
+ /// Gets the currently ongoing (not finished) span or null
if none available.
+ /// This relies on the transactions being manually set on the scope via .
+ ///
+ public ISpan? GetSpan() => Transaction?.GetLastActiveSpan() ?? Transaction;
}
}
diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs
index 89dd6e23bf..50026b7e42 100644
--- a/src/Sentry/SentryClient.cs
+++ b/src/Sentry/SentryClient.cs
@@ -103,7 +103,7 @@ public void CaptureUserFeedback(UserFeedback userFeedback)
}
///
- public void CaptureTransaction(Transaction transaction)
+ public void CaptureTransaction(ITransaction transaction)
{
if (_disposed)
{
@@ -129,8 +129,23 @@ public void CaptureTransaction(Transaction transaction)
return;
}
+ // Unfinished transaction can only happen if the user calls this method instead of
+ // transaction.Finish().
+ // We still send these transactions over, but warn the user not to do it.
+ if (!transaction.IsFinished)
+ {
+ _options.DiagnosticLogger?.LogWarning(
+ "Capturing a transaction which has not been finished. " +
+ "Please call transaction.Finish() instead of hub.CaptureTransaction(transaction) " +
+ "to properly finalize the transaction and send it to Sentry."
+ );
+ }
+
// Sampling decision MUST have been made at this point
- Debug.Assert(transaction.IsSampled != null, "Attempt to capture transaction without sampling decision.");
+ Debug.Assert(
+ transaction.IsSampled != null,
+ "Attempt to capture transaction without sampling decision."
+ );
if (transaction.IsSampled != true)
{
diff --git a/src/Sentry/SentryHttpMessageHandler.cs b/src/Sentry/SentryHttpMessageHandler.cs
new file mode 100644
index 0000000000..405f582706
--- /dev/null
+++ b/src/Sentry/SentryHttpMessageHandler.cs
@@ -0,0 +1,72 @@
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Sentry.Extensibility;
+using Sentry.Protocol;
+
+namespace Sentry
+{
+ ///
+ /// Special HTTP message handler that can be used to propagate Sentry headers and other contextual information.
+ ///
+ public class SentryHttpMessageHandler : HttpMessageHandler
+ {
+ private readonly HttpMessageInvoker _httpMessageInvoker;
+ private readonly IHub _hub;
+
+ private SentryHttpMessageHandler(HttpMessageInvoker httpMessageInvoker, IHub hub)
+ {
+ _httpMessageInvoker = httpMessageInvoker;
+ _hub = hub;
+ }
+
+ ///
+ /// Initializes an instance of .
+ ///
+ public SentryHttpMessageHandler(HttpMessageHandler innerHandler, IHub hub)
+ : this(new HttpMessageInvoker(innerHandler, false), hub) {}
+
+ ///
+ /// Initializes an instance of .
+ ///
+ public SentryHttpMessageHandler(HttpMessageHandler innerHandler)
+ : this(innerHandler, HubAdapter.Instance) {}
+
+ ///
+ /// Initializes an instance of .
+ ///
+ public SentryHttpMessageHandler(IHub hub)
+ : this(new HttpMessageInvoker(new HttpClientHandler(), true), hub) {}
+
+ ///
+ /// Initializes an instance of .
+ ///
+ public SentryHttpMessageHandler()
+ : this(HubAdapter.Instance) {}
+
+ ///
+ protected override Task SendAsync(
+ HttpRequestMessage request,
+ CancellationToken cancellationToken)
+ {
+ // Set trace header if it hasn't already been set
+ if (!request.Headers.Contains(SentryTraceHeader.HttpHeaderName) &&
+ _hub.GetTraceHeader() is {} traceHeader)
+ {
+ request.Headers.Add(
+ SentryTraceHeader.HttpHeaderName,
+ traceHeader.ToString()
+ );
+ }
+
+ return _httpMessageInvoker.SendAsync(request, cancellationToken);
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ _httpMessageInvoker.Dispose();
+ base.Dispose(disposing);
+ }
+ }
+}
diff --git a/src/Sentry/SentrySdk.cs b/src/Sentry/SentrySdk.cs
index d38ae20693..b94f5b80f9 100644
--- a/src/Sentry/SentrySdk.cs
+++ b/src/Sentry/SentrySdk.cs
@@ -312,7 +312,7 @@ public static void CaptureUserFeedback(SentryId eventId, string email, string co
/// Captures a transaction.
///
[DebuggerStepThrough]
- public static void CaptureTransaction(Transaction transaction)
+ public static void CaptureTransaction(ITransaction transaction)
=> _hub.CaptureTransaction(transaction);
///
@@ -345,11 +345,18 @@ public static ITransaction StartTransaction(string name, string operation)
public static ITransaction StartTransaction(string name, string operation, string description)
=> _hub.StartTransaction(name, operation, description);
+ ///
+ /// Starts a transaction.
+ ///
+ [DebuggerStepThrough]
+ public static ITransaction StartTransaction(string name, string operation, SentryTraceHeader traceHeader)
+ => _hub.StartTransaction(name, operation, traceHeader);
+
///
/// Gets the Sentry trace header.
///
[DebuggerStepThrough]
public static SentryTraceHeader? GetTraceHeader()
- => _hub.GetSentryTrace();
+ => _hub.GetTraceHeader();
}
}
diff --git a/test/Sentry.Testing/HttpClientExtensions.cs b/test/Sentry.Testing/HttpClientExtensions.cs
index 0675bb80f2..484651d653 100644
--- a/test/Sentry.Testing/HttpClientExtensions.cs
+++ b/test/Sentry.Testing/HttpClientExtensions.cs
@@ -38,17 +38,20 @@ public static async Task CloneAsync(this HttpRequestMessage
}
// Content
- var cloneContentStream = new MemoryStream();
+ if (source.Content != null)
+ {
+ var cloneContentStream = new MemoryStream();
- await source.Content.CopyToAsync(cloneContentStream).ConfigureAwait(false);
- cloneContentStream.Position = 0;
+ await source.Content.CopyToAsync(cloneContentStream).ConfigureAwait(false);
+ cloneContentStream.Position = 0;
- clone.Content = new StreamContent(cloneContentStream);
+ clone.Content = new StreamContent(cloneContentStream);
- // Content headers
- foreach (var (key, value) in source.Content.Headers)
- {
- clone.Content.Headers.Add(key, value);
+ // Content headers
+ foreach (var (key, value) in source.Content.Headers)
+ {
+ clone.Content.Headers.Add(key, value);
+ }
}
return clone;
diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs
index f78fa3ae13..2d5f822ff3 100644
--- a/test/Sentry.Tests/HubTests.cs
+++ b/test/Sentry.Tests/HubTests.cs
@@ -162,6 +162,30 @@ public void StartTransaction_NameOpDescription_Works()
transaction.Description.Should().Be("description");
}
+ [Fact]
+ public void StartTransaction_FromTraceHeader_Works()
+ {
+ // Arrange
+ var hub = new Hub(new SentryOptions
+ {
+ Dsn = DsnSamples.ValidDsnWithSecret
+ });
+
+ var traceHeader = new SentryTraceHeader(
+ SentryId.Parse("75302ac48a024bde9a3b3734a82e36c8"),
+ SpanId.Parse("2000000000000000"),
+ true
+ );
+
+ // Act
+ var transaction = hub.StartTransaction("name", "operation", traceHeader);
+
+ // Assert
+ transaction.TraceId.Should().Be(SentryId.Parse("75302ac48a024bde9a3b3734a82e36c8"));
+ transaction.ParentSpanId.Should().Be(SpanId.Parse("2000000000000000"));
+ transaction.IsSampled.Should().BeTrue();
+ }
+
[Fact]
public void StartTransaction_StaticSampling_SampledIn()
{
@@ -305,5 +329,53 @@ public void StartTransaction_DynamicSampling_FallbackToStatic_SampledOut()
// Assert
transaction.IsSampled.Should().BeFalse();
}
+
+ [Fact]
+ public void GetTraceHeader_ReturnsHeaderForActiveSpan()
+ {
+ // Arrange
+ var hub = new Hub(new SentryOptions
+ {
+ Dsn = DsnSamples.ValidDsnWithSecret
+ });
+
+ var transaction = hub.StartTransaction("foo", "bar");
+
+ // Act
+ hub.WithScope(scope =>
+ {
+ scope.Transaction = transaction;
+
+ var header = hub.GetTraceHeader();
+
+ // Assert
+ header.Should().NotBeNull();
+ header?.SpanId.Should().Be(transaction.SpanId);
+ header?.TraceId.Should().Be(transaction.TraceId);
+ header?.IsSampled.Should().Be(transaction.IsSampled);
+ });
+ }
+
+ [Fact]
+ public void CaptureTransaction_AfterTransactionFinishes_ResetsTransactionOnScope()
+ {
+ // Arrange
+ var client = Substitute.For();
+
+ var hub = new Hub(client, new SentryOptions
+ {
+ Dsn = DsnSamples.ValidDsnWithSecret
+ });
+
+ var transaction = hub.StartTransaction("foo", "bar");
+
+ hub.WithScope(scope => scope.Transaction = transaction);
+
+ // Act
+ transaction.Finish();
+
+ // Assert
+ hub.WithScope(scope => scope.Transaction.Should().BeNull());
+ }
}
}
diff --git a/test/Sentry.Tests/Protocol/SentryTraceHeaderTests.cs b/test/Sentry.Tests/Protocol/SentryTraceHeaderTests.cs
new file mode 100644
index 0000000000..af1b0c6e69
--- /dev/null
+++ b/test/Sentry.Tests/Protocol/SentryTraceHeaderTests.cs
@@ -0,0 +1,54 @@
+using FluentAssertions;
+using Sentry.Protocol;
+using Xunit;
+
+namespace Sentry.Tests.Protocol
+{
+ public class SentryTraceHeaderTests
+ {
+ [Fact]
+ public void Parse_WithoutSampled_Works()
+ {
+ // Arrange
+ const string headerValue = "75302ac48a024bde9a3b3734a82e36c8-1000000000000000";
+
+ // Act
+ var header = SentryTraceHeader.Parse(headerValue);
+
+ // Assert
+ header.TraceId.Should().Be(SentryId.Parse("75302ac48a024bde9a3b3734a82e36c8"));
+ header.SpanId.Should().Be(SpanId.Parse("1000000000000000"));
+ header.IsSampled.Should().BeNull();
+ }
+
+ [Fact]
+ public void Parse_WithSampledTrue_Works()
+ {
+ // Arrange
+ const string headerValue = "75302ac48a024bde9a3b3734a82e36c8-1000000000000000-1";
+
+ // Act
+ var header = SentryTraceHeader.Parse(headerValue);
+
+ // Assert
+ header.TraceId.Should().Be(SentryId.Parse("75302ac48a024bde9a3b3734a82e36c8"));
+ header.SpanId.Should().Be(SpanId.Parse("1000000000000000"));
+ header.IsSampled.Should().BeTrue();
+ }
+
+ [Fact]
+ public void Parse_WithSampledFalse_Works()
+ {
+ // Arrange
+ const string headerValue = "75302ac48a024bde9a3b3734a82e36c8-1000000000000000-0";
+
+ // Act
+ var header = SentryTraceHeader.Parse(headerValue);
+
+ // Assert
+ header.TraceId.Should().Be(SentryId.Parse("75302ac48a024bde9a3b3734a82e36c8"));
+ header.SpanId.Should().Be(SpanId.Parse("1000000000000000"));
+ header.IsSampled.Should().BeFalse();
+ }
+ }
+}
diff --git a/test/Sentry.Tests/ScopeTests.cs b/test/Sentry.Tests/ScopeTests.cs
index bee736a7ed..e8e14c9652 100644
--- a/test/Sentry.Tests/ScopeTests.cs
+++ b/test/Sentry.Tests/ScopeTests.cs
@@ -157,5 +157,58 @@ public void TransactionName_TransactionStarted_NameReturnsActualTransactionName(
scope.TransactionName.Should().Be("foo");
scope.TransactionName.Should().Be(scope.Transaction?.Name);
}
+
+ [Fact]
+ public void GetSpan_NoSpans_ReturnsTransaction()
+ {
+ // Arrange
+ var scope = new Scope();
+ var transaction = new Transaction(DisabledHub.Instance, "foo", "_");
+ scope.Transaction = transaction;
+
+ // Act
+ var span = scope.GetSpan();
+
+ // Assert
+ span.Should().Be(transaction);
+ }
+
+ [Fact]
+ public void GetSpan_FinishedSpans_ReturnsTransaction()
+ {
+ // Arrange
+ var scope = new Scope();
+
+ var transaction = new Transaction(DisabledHub.Instance, "foo", "_");
+ transaction.StartChild("123").Finish();
+ transaction.StartChild("456").Finish();
+
+ scope.Transaction = transaction;
+
+ // Act
+ var span = scope.GetSpan();
+
+ // Assert
+ span.Should().Be(transaction);
+ }
+
+ [Fact]
+ public void GetSpan_ActiveSpans_ReturnsSpan()
+ {
+ // Arrange
+ var scope = new Scope();
+
+ var transaction = new Transaction(DisabledHub.Instance, "foo", "_");
+ var activeSpan = transaction.StartChild("123");
+ transaction.StartChild("456").Finish();
+
+ scope.Transaction = transaction;
+
+ // Act
+ var span = scope.GetSpan();
+
+ // Assert
+ span.Should().Be(activeSpan);
+ }
}
}
diff --git a/test/Sentry.Tests/SentryClientTests.cs b/test/Sentry.Tests/SentryClientTests.cs
index 85a380d0bc..c10ff7b717 100644
--- a/test/Sentry.Tests/SentryClientTests.cs
+++ b/test/Sentry.Tests/SentryClientTests.cs
@@ -352,7 +352,11 @@ public void CaptureTransaction_SampledOut_Dropped()
sut,
"test name",
"test operation"
- ) {IsSampled = false});
+ )
+ {
+ IsSampled = false,
+ EndTimestamp = DateTimeOffset.Now // finished
+ });
// Assert
_ = sut.Worker.DidNotReceive().EnqueueEnvelope(Arg.Any());
@@ -370,7 +374,11 @@ public void CaptureTransaction_ValidTransaction_Sent()
sut,
"test name",
"test operation"
- ) {IsSampled = true}
+ )
+ {
+ IsSampled = true,
+ EndTimestamp = DateTimeOffset.Now // finished
+ }
);
// Assert
@@ -387,7 +395,11 @@ public void CaptureTransaction_NoSpanId_Ignored()
sut,
"test name",
"test operation"
- ) {IsSampled = true};
+ )
+ {
+ IsSampled = true,
+ EndTimestamp = DateTimeOffset.Now // finished
+ };
transaction.Contexts.Trace.SpanId = SpanId.Empty;
@@ -410,7 +422,11 @@ public void CaptureTransaction_NoName_Ignored()
sut,
null!,
"test operation"
- ) {IsSampled = true}
+ )
+ {
+ IsSampled = true,
+ EndTimestamp = DateTimeOffset.Now // finished
+ }
);
// Assert
@@ -429,13 +445,40 @@ public void CaptureTransaction_NoOperation_Ignored()
sut,
"test name",
null!
- ) {IsSampled = true}
+ )
+ {
+ IsSampled = true,
+ EndTimestamp = DateTimeOffset.Now // finished
+ }
);
// Assert
_ = sut.Worker.DidNotReceive().EnqueueEnvelope(Arg.Any());
}
+ [Fact]
+ public void CaptureTransaction_NotFinished_Sent()
+ {
+ // Arrange
+ var sut = _fixture.GetSut();
+
+ // Act
+ sut.CaptureTransaction(
+ new Transaction(
+ sut,
+ "test name",
+ "test operation"
+ )
+ {
+ IsSampled = true,
+ EndTimestamp = null // not finished
+ }
+ );
+
+ // Assert
+ _ = sut.Worker.Received(1).EnqueueEnvelope(Arg.Any());
+ }
+
[Fact]
public void CaptureTransaction_DisposedClient_ThrowsObjectDisposedException()
{
diff --git a/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs b/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs
new file mode 100644
index 0000000000..2d73e8f64f
--- /dev/null
+++ b/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs
@@ -0,0 +1,68 @@
+using System.Linq;
+using System.Net.Http;
+using System.Threading.Tasks;
+using FluentAssertions;
+using NSubstitute;
+using Sentry.Protocol;
+using Sentry.Testing;
+using Xunit;
+
+namespace Sentry.Tests
+{
+ public class SentryHttpMessageHandlerTests
+ {
+ [Fact]
+ public async Task SendAsync_SentryTraceHeaderNotSet_SetsHeader()
+ {
+ // Arrange
+ var hub = Substitute.For();
+
+ hub.GetTraceHeader().ReturnsForAnyArgs(
+ SentryTraceHeader.Parse("75302ac48a024bde9a3b3734a82e36c8-1000000000000000-0")
+ );
+
+ using var innerHandler = new FakeHttpClientHandler();
+ using var sentryHandler = new SentryHttpMessageHandler(innerHandler, hub);
+ using var client = new HttpClient(sentryHandler);
+
+ // Act
+ await client.GetAsync("https://example.com");
+
+ using var request = innerHandler.GetRequests().Single();
+
+ // Assert
+ request.Headers.Should().Contain(h =>
+ h.Key == "sentry-trace" &&
+ string.Concat(h.Value) == "75302ac48a024bde9a3b3734a82e36c8-1000000000000000-0"
+ );
+ }
+
+ [Fact]
+ public async Task SendAsync_SentryTraceHeaderAlreadySet_NotOverwritten()
+ {
+ // Arrange
+ var hub = Substitute.For();
+
+ hub.GetTraceHeader().ReturnsForAnyArgs(
+ SentryTraceHeader.Parse("75302ac48a024bde9a3b3734a82e36c8-1000000000000000-0")
+ );
+
+ using var innerHandler = new FakeHttpClientHandler();
+ using var sentryHandler = new SentryHttpMessageHandler(innerHandler, hub);
+ using var client = new HttpClient(sentryHandler);
+
+ client.DefaultRequestHeaders.Add("sentry-trace", "foobar");
+
+ // Act
+ await client.GetAsync("https://example.com");
+
+ using var request = innerHandler.GetRequests().Single();
+
+ // Assert
+ request.Headers.Should().Contain(h =>
+ h.Key == "sentry-trace" &&
+ string.Concat(h.Value) == "foobar"
+ );
+ }
+ }
+}