Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate trace headers #758

Merged
merged 16 commits into from
Jan 26, 2021
21 changes: 21 additions & 0 deletions src/Sentry/HubExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,27 @@ public static ITransaction StartTransaction(
return transaction;
}

/// <summary>
/// Starts a transaction from the specified trace header.
/// </summary>
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);
}

/// <summary>
/// Adds a breadcrumb to the current scope.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions src/Sentry/Internal/Hub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ public ITransaction StartTransaction(

public SentryTraceHeader? GetSentryTrace()
{
// TODO:
var (currentScope, _) = ScopeManager.GetCurrent();
return currentScope.Transaction?.GetTraceHeader();
}
Expand Down
59 changes: 49 additions & 10 deletions src/Sentry/Protocol/SentryTraceHeader.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;

namespace Sentry.Protocol
{
Expand All @@ -7,24 +10,60 @@ namespace Sentry.Protocol
/// </summary>
public class SentryTraceHeader
{
private readonly SentryId _traceId;
private readonly SpanId _spanId;
private readonly bool? _isSampled;
private const string HeaderName = "sentry-trace";

/// <summary>
/// Trace ID.
/// </summary>
public SentryId TraceId { get; }

/// <summary>
/// Span ID.
/// </summary>
public SpanId SpanId { get; }

/// <summary>
/// Whether the trace is sampled.
/// </summary>
public bool? IsSampled { get; }

/// <summary>
/// Initializes an instance of <see cref="SentryTraceHeader"/>.
/// </summary>
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;
}

/// <summary>
/// Injects trace information into HTTP headers.
/// </summary>
public void Inject(HttpHeaders headers)
{
var headerValue = ToString();

headers.Remove(HeaderName);
headers.Add(HeaderName, headerValue);
}

/// <summary>
/// Injects trace information into the headers of the specified HTTP request.
/// </summary>
public void Inject(HttpRequestMessage request) =>
Inject(request.Headers);

/// <summary>
/// Injects trace information into the default headers of the specified HTTP client.
/// </summary>
public void Inject(HttpClient client) =>
Inject(client.DefaultRequestHeaders);

/// <inheritdoc />
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}";

/// <summary>
/// Parses <see cref="SentryTraceHeader"/> from string.
Expand Down
7 changes: 4 additions & 3 deletions src/Sentry/Protocol/TransactionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ public TransactionContext(
/// Initializes an instance of <see cref="TransactionContext"/>.
/// </summary>
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)
{
}

Expand All @@ -44,7 +45,7 @@ public TransactionContext(
string name,
string operation,
bool? isSampled)
: this(name, operation, "", isSampled)
: this(null, SentryId.Create(), name, operation, isSampled)
{
}

Expand Down
7 changes: 7 additions & 0 deletions src/Sentry/SentrySdk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,13 @@ public static ITransaction StartTransaction(string name, string operation)
public static ITransaction StartTransaction(string name, string operation, string description)
=> _hub.StartTransaction(name, operation, description);

/// <summary>
/// Starts a transaction.
/// </summary>
[DebuggerStepThrough]
public static ITransaction StartTransaction(string name, string operation, SentryTraceHeader traceHeader)
=> _hub.StartTransaction(name, operation, traceHeader);

/// <summary>
/// Gets the Sentry trace header.
/// </summary>
Expand Down
19 changes: 11 additions & 8 deletions test/Sentry.Testing/HttpClientExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,20 @@ public static async Task<HttpRequestMessage> 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;
Expand Down
24 changes: 24 additions & 0 deletions test/Sentry.Tests/HubTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
97 changes: 97 additions & 0 deletions test/Sentry.Tests/Protocol/SentryTraceHeaderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using FluentAssertions;
using Sentry.Protocol;
using Sentry.Testing;
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();
}

[Fact]
public void Inject_ToHttpRequest_Works()
{
// Arrange
using var request = new HttpRequestMessage();
var header = SentryTraceHeader.Parse("75302ac48a024bde9a3b3734a82e36c8-1000000000000000-0");

// Act
header.Inject(request);

// Assert
request.Headers.Should().Contain(h =>
h.Key == "sentry-trace" &&
string.Concat(h.Value) == "75302ac48a024bde9a3b3734a82e36c8-1000000000000000-0"
);
}

[Fact]
public async Task Inject_ToHttpClient_Works()
{
// Arrange
using var handler = new FakeHttpClientHandler();
using var client = new HttpClient(handler);
var header = SentryTraceHeader.Parse("75302ac48a024bde9a3b3734a82e36c8-1000000000000000-0");

// Act
header.Inject(client);
await client.GetAsync("https://example.com");

using var request = handler.GetRequests().Single();

// Assert
request.Headers.Should().Contain(h =>
h.Key == "sentry-trace" &&
string.Concat(h.Value) == "75302ac48a024bde9a3b3734a82e36c8-1000000000000000-0"
);
}
}
}