Skip to content

Commit 8cb62cf

Browse files
authored
feat: add propagateTraceparent option (#1161)
1 parent d7582e8 commit 8cb62cf

File tree

5 files changed

+107
-4
lines changed

5 files changed

+107
-4
lines changed

client.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,9 @@ type ClientOptions struct {
145145
TracesSampler TracesSampler
146146
// Control with URLs trace propagation should be enabled. Does not support regex patterns.
147147
TracePropagationTargets []string
148+
// PropagateTraceparent is used to control whether the W3C Trace Context HTTP traceparent header
149+
// is propagated on outgoing http requests.
150+
PropagateTraceparent bool
148151
// List of regexp strings that will be used to match against event's message
149152
// and if applicable, caught errors type and value.
150153
// If the match is found, then a whole event will be dropped.

httpclient/sentryhttpclient.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,22 @@ func NewSentryRoundTripper(originalRoundTripper http.RoundTripper, opts ...Sentr
4444

4545
// Configure trace propagation targets
4646
var tracePropagationTargets []string
47+
var propagateTraceparent bool
4748
if hub := sentry.CurrentHub(); hub != nil {
4849
client := hub.Client()
4950
if client != nil {
5051
clientOptions := client.Options()
5152
if clientOptions.TracePropagationTargets != nil {
5253
tracePropagationTargets = clientOptions.TracePropagationTargets
5354
}
55+
propagateTraceparent = clientOptions.PropagateTraceparent
5456
}
5557
}
5658

5759
t := &SentryRoundTripper{
5860
originalRoundTripper: originalRoundTripper,
5961
tracePropagationTargets: tracePropagationTargets,
62+
propagateTraceparent: propagateTraceparent,
6063
}
6164

6265
for _, opt := range opts {
@@ -72,6 +75,7 @@ func NewSentryRoundTripper(originalRoundTripper http.RoundTripper, opts ...Sentr
7275
type SentryRoundTripper struct {
7376
originalRoundTripper http.RoundTripper
7477

78+
propagateTraceparent bool
7579
tracePropagationTargets []string
7680
}
7781

@@ -96,8 +100,11 @@ func (s *SentryRoundTripper) RoundTrip(request *http.Request) (*http.Response, e
96100
parentSpan := sentry.SpanFromContext(request.Context())
97101
if parentSpan == nil {
98102
if hub := sentry.GetHubFromContext(request.Context()); hub != nil {
99-
request.Header.Add("Baggage", hub.GetBaggage())
100-
request.Header.Add("Sentry-Trace", hub.GetTraceparent())
103+
request.Header.Add(sentry.SentryBaggageHeader, hub.GetBaggage())
104+
request.Header.Add(sentry.SentryTraceHeader, hub.GetTraceparent())
105+
if s.propagateTraceparent {
106+
request.Header.Add(sentry.TraceparentHeader, hub.GetTraceparentW3C())
107+
}
101108
}
102109

103110
return s.originalRoundTripper.RoundTrip(request)
@@ -115,8 +122,11 @@ func (s *SentryRoundTripper) RoundTrip(request *http.Request) (*http.Response, e
115122
span.SetData("server.port", request.URL.Port())
116123

117124
// Always add `Baggage` and `Sentry-Trace` headers.
118-
request.Header.Add("Baggage", span.ToBaggage())
119-
request.Header.Add("Sentry-Trace", span.ToSentryTrace())
125+
request.Header.Add(sentry.SentryBaggageHeader, span.ToBaggage())
126+
request.Header.Add(sentry.SentryTraceHeader, span.ToSentryTrace())
127+
if s.propagateTraceparent {
128+
request.Header.Add(sentry.TraceparentHeader, span.ToTraceparent())
129+
}
120130

121131
response, err := s.originalRoundTripper.RoundTrip(request)
122132
if err != nil {

httpclient/sentryhttpclient_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"crypto/rand"
77
"crypto/tls"
88
"errors"
9+
"fmt"
910
"io"
1011
"net/http"
1112
"strconv"
@@ -467,6 +468,58 @@ func TestIntegration_NoParentSpan(t *testing.T) {
467468
}
468469
}
469470

471+
func TestPropagateTraceparentHeader(t *testing.T) {
472+
err := sentry.Init(sentry.ClientOptions{
473+
EnableTracing: true,
474+
TracesSampleRate: 1.0,
475+
PropagateTraceparent: true,
476+
BeforeSendTransaction: func(event *sentry.Event, _ *sentry.EventHint) *sentry.Event { return event },
477+
})
478+
if err != nil {
479+
t.Fatal(err)
480+
}
481+
482+
span := sentry.StartSpan(context.Background(), "fake_parent", sentry.WithTransactionName("Fake Parent"))
483+
ctx := span.Context()
484+
485+
request, err := http.NewRequestWithContext(ctx, "GET", "https://example.com/foo", nil)
486+
if err != nil {
487+
t.Fatal(err)
488+
}
489+
490+
roundTripper := &noopRoundTripper{
491+
ExpectResponseStatus: 200,
492+
ExpectResponseLength: 0,
493+
}
494+
495+
client := &http.Client{
496+
Transport: sentryhttpclient.NewSentryRoundTripper(roundTripper),
497+
}
498+
499+
response, err := client.Do(request)
500+
if err != nil {
501+
t.Fatal(err)
502+
}
503+
if response.Body != nil {
504+
response.Body.Close()
505+
}
506+
span.Finish()
507+
508+
traceparent := response.Request.Header.Get("traceparent")
509+
if traceparent == "" {
510+
t.Fatalf(`Expected "traceparent" header to be set`)
511+
}
512+
513+
sentryTrace := response.Request.Header.Get("Sentry-Trace")
514+
if sentryTrace == "" {
515+
t.Fatalf(`Expected "Sentry-Trace" header to be set`)
516+
}
517+
518+
if want := traceparentFromSentryTraceHeader(t, sentryTrace); traceparent != want {
519+
t.Fatalf(`Unexpected "traceparent" header value, got %q want %q`, traceparent, want)
520+
}
521+
}
522+
470523
func TestDefaults(t *testing.T) {
471524
t.Run("Create a regular outgoing HTTP request with default NewSentryRoundTripper", func(t *testing.T) {
472525
roundTripper := sentryhttpclient.NewSentryRoundTripper(nil)
@@ -482,3 +535,19 @@ func TestDefaults(t *testing.T) {
482535
}
483536
})
484537
}
538+
539+
func traceparentFromSentryTraceHeader(t *testing.T, sentryTrace string) string {
540+
t.Helper()
541+
542+
traceParentContext, valid := sentry.ParseTraceParentContext([]byte(sentryTrace))
543+
if !valid {
544+
t.Fatalf("Invalid sentry-trace header: %q", sentryTrace)
545+
}
546+
547+
traceFlags := "00"
548+
if traceParentContext.Sampled == sentry.SampledTrue {
549+
traceFlags = "01"
550+
}
551+
552+
return fmt.Sprintf("00-%s-%s-%s", traceParentContext.TraceID.String(), traceParentContext.ParentSpanID.String(), traceFlags)
553+
}

hub.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,17 @@ func (hub *Hub) GetTraceparent() string {
393393
return fmt.Sprintf("%s-%s", scope.propagationContext.TraceID, scope.propagationContext.SpanID)
394394
}
395395

396+
// GetTraceparentW3C returns the current traceparent string in W3C format.
397+
// This is intended for propagation to downstream services that expect the W3C header.
398+
func (hub *Hub) GetTraceparentW3C() string {
399+
scope := hub.Scope()
400+
if scope.span != nil {
401+
return scope.span.ToTraceparent()
402+
}
403+
404+
return fmt.Sprintf("00-%s-%s-00", scope.propagationContext.TraceID, scope.propagationContext.SpanID)
405+
}
406+
396407
// GetBaggage returns the current Sentry baggage string, to be used as a HTTP header value
397408
// or HTML meta tag value.
398409
// This function is context aware, as in it either returns the baggage based

tracing.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
const (
2020
SentryTraceHeader = "sentry-trace"
2121
SentryBaggageHeader = "baggage"
22+
TraceparentHeader = "traceparent"
2223
)
2324

2425
// SpanOrigin indicates what created a trace or a span. See: https://develop.sentry.dev/sdk/performance/trace-origin/
@@ -320,6 +321,15 @@ func (s *Span) ToSentryTrace() string {
320321
return b.String()
321322
}
322323

324+
// ToTraceparent returns the W3C traceparent header value for the span.
325+
func (s *Span) ToTraceparent() string {
326+
traceFlags := "00"
327+
if s.Sampled == SampledTrue {
328+
traceFlags = "01"
329+
}
330+
return fmt.Sprintf("00-%s-%s-%s", s.TraceID.String(), s.SpanID.String(), traceFlags)
331+
}
332+
323333
// ToBaggage returns the serialized DynamicSamplingContext from a transaction.
324334
// Use this function to propagate the DynamicSamplingContext to a downstream SDK,
325335
// either as the value of the "baggage" HTTP header, or as an html "baggage" meta tag.

0 commit comments

Comments
 (0)