diff --git a/client.go b/client.go index 28160af0..dd5e9826 100644 --- a/client.go +++ b/client.go @@ -90,7 +90,7 @@ type EventProcessor func(event *Event, hint *EventHint) *Event // ApplyToEvent changes an event based on external data and/or // an event hint. type EventModifier interface { - ApplyToEvent(event *Event, hint *EventHint) *Event + ApplyToEvent(event *Event, hint *EventHint, client *Client) *Event } var globalEventProcessors []EventProcessor @@ -690,7 +690,7 @@ func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventMod } if scope != nil { - event = scope.ApplyToEvent(event, hint) + event = scope.ApplyToEvent(event, hint, client) if event == nil { return nil } diff --git a/dynamic_sampling_context.go b/dynamic_sampling_context.go index 36507260..1eba2a28 100644 --- a/dynamic_sampling_context.go +++ b/dynamic_sampling_context.go @@ -78,15 +78,7 @@ func DynamicSamplingContextFromTransaction(span *Span) DynamicSamplingContext { } } - if userSegment := scope.user.Segment; userSegment != "" { - entries["user_segment"] = userSegment - } - - if span.Sampled.Bool() { - entries["sampled"] = "true" - } else { - entries["sampled"] = "false" - } + entries["sampled"] = strconv.FormatBool(span.Sampled.Bool()) return DynamicSamplingContext{ Entries: entries, @@ -121,3 +113,43 @@ func (d DynamicSamplingContext) String() string { return "" } + +// Constructs a new DynamicSamplingContext using a scope and client. Accessing +// fields on the scope are not thread safe, and this function should only be +// called within scope methods. +func DynamicSamplingContextFromScope(scope *Scope, client *Client) DynamicSamplingContext { + entries := map[string]string{} + + if client == nil || scope == nil { + return DynamicSamplingContext{ + Entries: entries, + Frozen: false, + } + } + + propagationContext := scope.propagationContext + + if traceID := propagationContext.TraceID.String(); traceID != "" { + entries["trace_id"] = traceID + } + if sampleRate := client.options.TracesSampleRate; sampleRate != 0 { + entries["sample_rate"] = strconv.FormatFloat(sampleRate, 'f', -1, 64) + } + + if dsn := client.dsn; dsn != nil { + if publicKey := dsn.publicKey; publicKey != "" { + entries["public_key"] = publicKey + } + } + if release := client.options.Release; release != "" { + entries["release"] = release + } + if environment := client.options.Environment; environment != "" { + entries["environment"] = environment + } + + return DynamicSamplingContext{ + Entries: entries, + Frozen: true, + } +} diff --git a/dynamic_sampling_context_test.go b/dynamic_sampling_context_test.go index a6604ee1..bb636a06 100644 --- a/dynamic_sampling_context_test.go +++ b/dynamic_sampling_context_test.go @@ -97,14 +97,13 @@ func TestDynamicSamplingContextFromTransaction(t *testing.T) { want: DynamicSamplingContext{ Frozen: true, Entries: map[string]string{ - "sample_rate": "1", - "trace_id": "d49d9bf66f13450b81f65bc51cf49c03", - "public_key": "public", - "release": "1.0.0", - "environment": "test", - "transaction": "name", - "user_segment": "user_segment", - "sampled": "true", + "sample_rate": "1", + "trace_id": "d49d9bf66f13450b81f65bc51cf49c03", + "public_key": "public", + "release": "1.0.0", + "environment": "test", + "transaction": "name", + "sampled": "true", }, }, }, @@ -181,3 +180,68 @@ func TestString(t *testing.T) { } testutils.AssertBaggageStringsEqual(t, dsc.String(), "sentry-trace_id=d49d9bf66f13450b81f65bc51cf49c03,sentry-public_key=public,sentry-sample_rate=1") } + +func TestDynamicSamplingContextFromScope(t *testing.T) { + tests := map[string]struct { + scope *Scope + client *Client + expected DynamicSamplingContext + }{ + "Valid input": { + scope: &Scope{ + propagationContext: PropagationContext{ + TraceID: TraceIDFromHex("d49d9bf66f13450b81f65bc51cf49c03"), + SpanID: SpanIDFromHex("a9f442f9330b4e09"), + }, + }, + client: func() *Client { + dsn, _ := NewDsn("http://public@example.com/sentry/1") + return &Client{ + options: ClientOptions{ + Dsn: dsn.String(), + Release: "1.0.0", + Environment: "production", + }, + dsn: dsn, + } + }(), + expected: DynamicSamplingContext{ + Entries: map[string]string{ + "trace_id": "d49d9bf66f13450b81f65bc51cf49c03", + "public_key": "public", + "release": "1.0.0", + "environment": "production", + }, + Frozen: true, + }, + }, + "Nil client": { + scope: &Scope{ + propagationContext: PropagationContext{ + TraceID: TraceIDFromHex("d49d9bf66f13450b81f65bc51cf49c03"), + SpanID: SpanIDFromHex("a9f442f9330b4e09"), + }, + }, + client: nil, + expected: DynamicSamplingContext{ + Entries: map[string]string{}, + Frozen: false, + }, + }, + "Nil scope": { + scope: nil, + client: &Client{}, + expected: DynamicSamplingContext{ + Entries: map[string]string{}, + Frozen: false, + }, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + result := DynamicSamplingContextFromScope(tt.scope, tt.client) + assertEqual(t, tt.expected, result) + }) + } +} diff --git a/http/sentryhttp.go b/http/sentryhttp.go index a8720a29..0eb998c5 100644 --- a/http/sentryhttp.go +++ b/http/sentryhttp.go @@ -121,6 +121,7 @@ func (h *Handler) handle(handler http.Handler) http.HandlerFunc { // level?, ...). r = r.WithContext(transaction.Context()) hub.Scope().SetRequest(r) + defer h.recoverWithSentry(hub, r) handler.ServeHTTP(rw, r) } diff --git a/hub.go b/hub.go index 6af1d5af..5c9fc800 100644 --- a/hub.go +++ b/hub.go @@ -365,6 +365,27 @@ func (hub *Hub) Flush(timeout time.Duration) bool { return client.Flush(timeout) } +// Continue a trace based on HTTP header values. If performance is enabled this +// returns a SpanOption that can be used to start a transaction, otherwise nil. +func (hub *Hub) ContinueTrace(trace, baggage string) (SpanOption, error) { + scope := hub.Scope() + propagationContext, err := PropagationContextFromHeaders(trace, baggage) + if err != nil { + return nil, err + } + + scope.SetPropagationContext(propagationContext) + + client := hub.Client() + if client != nil && client.options.EnableTracing { + return ContinueFromHeaders(trace, baggage), nil + } + + scope.SetContext("trace", propagationContext.Map()) + + return nil, nil +} + // HasHubOnContext checks whether Hub instance is bound to a given Context struct. func HasHubOnContext(ctx context.Context) bool { _, ok := ctx.Value(HubContextKey).(*Hub) diff --git a/hub_test.go b/hub_test.go index 46b88977..79ea2671 100644 --- a/hub_test.go +++ b/hub_test.go @@ -2,12 +2,14 @@ package sentry import ( "context" + "errors" "fmt" "sync" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/assert" ) const testDsn = "http://whatever@example.com/1337" @@ -324,6 +326,88 @@ func TestHasHubOnContextReturnsFalseIfHubIsNotThere(t *testing.T) { assertEqual(t, false, HasHubOnContext(ctx)) } +func TestHub_ContinueTrace(t *testing.T) { + newScope := func() *Scope { + return &Scope{contexts: make(map[string]Context)} + } + + mockClient := &Client{options: ClientOptions{EnableTracing: true}} + + tests := map[string]struct { + hub *Hub + trace string + baggage string + expectedErr error + expectedSpan bool // Whether a SpanOption is expected to be returned + checkScope func(t *testing.T, scope *Scope) // Additional checks on the scope + }{ + "Valid trace and baggage": { + hub: NewHub(mockClient, newScope()), + trace: "4fbfb1b884c8532962a3c0b7b834428e-a9f442f9330b4e09", + baggage: "sentry-release=1.0.0,sentry-environment=production", + expectedErr: nil, + expectedSpan: true, + checkScope: func(t *testing.T, scope *Scope) { + assert.Equal(t, "4fbfb1b884c8532962a3c0b7b834428e", scope.propagationContext.TraceID.String()) + }, + }, + "Invalid trace": { + hub: NewHub(mockClient, newScope()), + trace: "invalid", + baggage: "sentry-release=1.0.0,sentry-environment=production", + expectedErr: nil, + expectedSpan: true, + checkScope: func(t *testing.T, scope *Scope) { + assert.NotEmpty(t, scope.propagationContext.TraceID.String()) + }, + }, + "Invalid baggage": { + hub: NewHub(mockClient, newScope()), + trace: "4fbfb1b884c8532962a3c0b7b834428e-a9f442f9330b4e09", + baggage: "invalid_baggage", + expectedErr: errors.New("invalid baggage list-member: \"invalid_baggage\""), + expectedSpan: false, + checkScope: func(t *testing.T, scope *Scope) { + assert.Equal(t, "00000000000000000000000000000000", scope.propagationContext.TraceID.String()) + }, + }, + "Tracing not enabled": { + hub: NewHub(&Client{options: ClientOptions{EnableTracing: false}}, newScope()), + trace: "4fbfb1b884c8532962a3c0b7b834428e-a9f442f9330b4e09", + baggage: "sentry-release=1.0.0,sentry-environment=production", + expectedErr: nil, + expectedSpan: false, + checkScope: func(t *testing.T, scope *Scope) { + assert.Equal(t, "4fbfb1b884c8532962a3c0b7b834428e", scope.propagationContext.TraceID.String()) + assert.Contains(t, scope.contexts, "trace") + }, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + opt, err := tt.hub.ContinueTrace(tt.trace, tt.baggage) + + if tt.expectedErr != nil { + assert.Error(t, err, "expected error, got nil") + assert.Equal(t, tt.expectedErr.Error(), err.Error()) + } else { + assert.NoError(t, err, "expected no error, got one") + } + + // Check for expected SpanOption + if tt.expectedSpan { + assert.NotNil(t, opt, "expected SpanOption, got nil") + } else { + assert.Nil(t, opt, "expected no SpanOption, got one") + } + + // Additional checks on the scope + tt.checkScope(t, tt.hub.Scope()) + }) + } +} + func TestGetHubFromContext(t *testing.T) { hub, _, _ := setupHubTest() ctx := context.Background() diff --git a/mocks_test.go b/mocks_test.go index a87bd3f4..ccb98295 100644 --- a/mocks_test.go +++ b/mocks_test.go @@ -14,7 +14,7 @@ func (scope *ScopeMock) AddBreadcrumb(breadcrumb *Breadcrumb, _ int) { scope.breadcrumb = breadcrumb } -func (scope *ScopeMock) ApplyToEvent(event *Event, _ *EventHint) *Event { +func (scope *ScopeMock) ApplyToEvent(event *Event, _ *EventHint, _ *Client) *Event { if scope.shouldDropEvent { return nil } diff --git a/propagation_context.go b/propagation_context.go new file mode 100644 index 00000000..7a0766a8 --- /dev/null +++ b/propagation_context.go @@ -0,0 +1,90 @@ +package sentry + +import ( + "crypto/rand" + "encoding/json" +) + +type PropagationContext struct { + TraceID TraceID `json:"trace_id"` + SpanID SpanID `json:"span_id"` + ParentSpanID SpanID `json:"parent_span_id"` + DynamicSamplingContext DynamicSamplingContext `json:"-"` +} + +func (p PropagationContext) MarshalJSON() ([]byte, error) { + type propagationContext PropagationContext + var parentSpanID string + if p.ParentSpanID != zeroSpanID { + parentSpanID = p.ParentSpanID.String() + } + return json.Marshal(struct { + *propagationContext + ParentSpanID string `json:"parent_span_id,omitempty"` + }{ + propagationContext: (*propagationContext)(&p), + ParentSpanID: parentSpanID, + }) +} + +func (p PropagationContext) Map() map[string]interface{} { + m := map[string]interface{}{ + "trace_id": p.TraceID, + "span_id": p.SpanID, + } + + if p.ParentSpanID != zeroSpanID { + m["parent_span_id"] = p.ParentSpanID + } + + return m +} + +func NewPropagationContext() PropagationContext { + p := PropagationContext{} + + if _, err := rand.Read(p.TraceID[:]); err != nil { + panic(err) + } + + if _, err := rand.Read(p.SpanID[:]); err != nil { + panic(err) + } + + return p +} + +func PropagationContextFromHeaders(trace, baggage string) (PropagationContext, error) { + p := NewPropagationContext() + + if _, err := rand.Read(p.SpanID[:]); err != nil { + panic(err) + } + + hasTrace := false + if trace != "" { + if tpc, valid := ParseTraceParentContext([]byte(trace)); valid { + hasTrace = true + p.TraceID = tpc.TraceID + p.ParentSpanID = tpc.ParentSpanID + } + } + + if baggage != "" { + dsc, err := DynamicSamplingContextFromHeader([]byte(baggage)) + if err != nil { + return PropagationContext{}, err + } + p.DynamicSamplingContext = dsc + } + + // In case a sentry-trace header is present but there are no sentry-related + // values in the baggage, create an empty, frozen DynamicSamplingContext. + if hasTrace && !p.DynamicSamplingContext.HasEntries() { + p.DynamicSamplingContext = DynamicSamplingContext{ + Frozen: true, + } + } + + return p, nil +} diff --git a/propagation_context_test.go b/propagation_context_test.go new file mode 100644 index 00000000..2fb303b1 --- /dev/null +++ b/propagation_context_test.go @@ -0,0 +1,145 @@ +package sentry + +import ( + "bytes" + "encoding/json" + "testing" +) + +func TestPropagationContextMarshalJSON(t *testing.T) { + v := NewPropagationContext() + b, err := json.Marshal(v) + if err != nil { + t.Fatal(err) + } + if bytes.Contains(b, []byte("parent_span_id")) { + t.Fatalf("unwanted parent_span_id: %s", b) + } + + v.ParentSpanID = SpanIDFromHex("b72fa28504b07285") + b2, err := json.Marshal(v) + if err != nil { + t.Fatal(err) + } + if !bytes.Contains(b2, []byte("parent_span_id")) { + t.Fatalf("missing parent_span_id: %s", b) + } +} + +func TestPropagationContextMap(t *testing.T) { + p := NewPropagationContext() + assertEqual(t, + p.Map(), + map[string]interface{}{ + "trace_id": p.TraceID, + "span_id": p.SpanID, + }, + "without parent span id") + + p.ParentSpanID = SpanIDFromHex("b72fa28504b07285") + assertEqual(t, + p.Map(), + map[string]interface{}{ + "trace_id": p.TraceID, + "span_id": p.SpanID, + "parent_span_id": p.ParentSpanID, + }, + "without praent span id") +} + +func TestPropagationContextFromHeaders(t *testing.T) { + tests := []struct { + traceStr string + baggageStr string + want PropagationContext + }{ + { + // No sentry-trace or baggage => nothing to do, unfrozen DSC + traceStr: "", + baggageStr: "", + want: PropagationContext{ + DynamicSamplingContext: DynamicSamplingContext{ + Frozen: false, + Entries: nil, + }, + }, + }, + { + // Third-party baggage => nothing to do, unfrozen DSC + traceStr: "", + baggageStr: "other-vendor-key1=value1;value2, other-vendor-key2=value3", + want: PropagationContext{ + DynamicSamplingContext: DynamicSamplingContext{ + Frozen: false, + Entries: map[string]string{}, + }, + }, + }, + { + // sentry-trace and no baggage => we should create a new DSC and freeze it + // immediately. + traceStr: "bc6d53f15eb88f4320054569b8c553d4-b72fa28504b07285-1", + baggageStr: "", + want: PropagationContext{ + TraceID: TraceIDFromHex("bc6d53f15eb88f4320054569b8c553d4"), + ParentSpanID: SpanIDFromHex("b72fa28504b07285"), + DynamicSamplingContext: DynamicSamplingContext{ + Frozen: true, + }, + }, + }, + { + traceStr: "bc6d53f15eb88f4320054569b8c553d4-b72fa28504b07285-1", + baggageStr: "sentry-trace_id=d49d9bf66f13450b81f65bc51cf49c03,sentry-public_key=public,sentry-sample_rate=1", + want: PropagationContext{ + TraceID: TraceIDFromHex("bc6d53f15eb88f4320054569b8c553d4"), + ParentSpanID: SpanIDFromHex("b72fa28504b07285"), + DynamicSamplingContext: DynamicSamplingContext{ + Frozen: true, + Entries: map[string]string{ + "public_key": "public", + "sample_rate": "1", + "trace_id": "d49d9bf66f13450b81f65bc51cf49c03", + }, + }, + }, + }, + } + + for _, tt := range tests { + p, err := PropagationContextFromHeaders(tt.traceStr, tt.baggageStr) + if err != nil { + t.Fatal(err) + } + + if tt.want.TraceID != zeroTraceID && p.TraceID != tt.want.TraceID { + t.Errorf("got TraceID = %s, want %s", p.TraceID, tt.want.TraceID) + } + + if p.TraceID == zeroTraceID { + t.Errorf("got TraceID = %s, want non-zero", p.TraceID) + } + + if p.ParentSpanID != tt.want.ParentSpanID { + t.Errorf("got ParentSpanID = %s, want %s", p.ParentSpanID, tt.want.ParentSpanID) + } + + if p.SpanID == zeroSpanID { + t.Errorf("got SpanID = %s, want non-zero", p.SpanID) + } + + assertEqual(t, p.DynamicSamplingContext, tt.want.DynamicSamplingContext) + } +} + +func TestNewPropagationContext(t *testing.T) { + context := NewPropagationContext() + + if context.TraceID == zeroTraceID { + t.Errorf("TraceID should not be zero") + } + + if context.SpanID == zeroSpanID { + t.Errorf("SpanID should not be zero") + } +} diff --git a/scope.go b/scope.go index 374010b6..74eab95e 100644 --- a/scope.go +++ b/scope.go @@ -43,20 +43,22 @@ type Scope struct { Overflow() bool } eventProcessors []EventProcessor + + propagationContext PropagationContext + span *Span } // NewScope creates a new Scope. func NewScope() *Scope { - scope := Scope{ - breadcrumbs: make([]*Breadcrumb, 0), - attachments: make([]*Attachment, 0), - tags: make(map[string]string), - contexts: make(map[string]Context), - extra: make(map[string]interface{}), - fingerprint: make([]string, 0), + return &Scope{ + breadcrumbs: make([]*Breadcrumb, 0), + attachments: make([]*Attachment, 0), + tags: make(map[string]string), + contexts: make(map[string]Context), + extra: make(map[string]interface{}), + fingerprint: make([]string, 0), + propagationContext: NewPropagationContext(), } - - return &scope } // AddBreadcrumb adds new breadcrumb to the current scope @@ -292,6 +294,22 @@ func (scope *Scope) SetLevel(level Level) { scope.level = level } +// SetPropagationContext sets the propagation context for the current scope. +func (scope *Scope) SetPropagationContext(propagationContext PropagationContext) { + scope.mu.Lock() + defer scope.mu.Unlock() + + scope.propagationContext = propagationContext +} + +// SetSpan sets a span for the current scope. +func (scope *Scope) SetSpan(span *Span) { + scope.mu.Lock() + defer scope.mu.Unlock() + + scope.span = span +} + // Clone returns a copy of the current scope with all data copied over. func (scope *Scope) Clone() *Scope { scope.mu.RLock() @@ -318,6 +336,8 @@ func (scope *Scope) Clone() *Scope { clone.request = scope.request clone.requestBody = scope.requestBody clone.eventProcessors = scope.eventProcessors + clone.propagationContext = scope.propagationContext + clone.span = scope.span return clone } @@ -335,7 +355,7 @@ func (scope *Scope) AddEventProcessor(processor EventProcessor) { } // ApplyToEvent takes the data from the current scope and attaches it to the event. -func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint) *Event { +func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint, client *Client) *Event { scope.mu.RLock() defer scope.mu.RUnlock() @@ -379,6 +399,31 @@ func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint) *Event { } } + // Apply the trace context to errors if there is a Span on the scope. If + // there isn't then fall back to the propagation context. + if event.Type != transactionType { + if event.Contexts == nil { + event.Contexts = make(map[string]Context) + } + + if scope.span != nil { + event.Contexts["trace"] = scope.span.traceContext().Map() + + transaction := scope.span.GetTransaction() + if transaction != nil { + event.sdkMetaData.dsc = DynamicSamplingContextFromTransaction(transaction) + } + } else { + event.Contexts["trace"] = scope.propagationContext.Map() + + dsc := scope.propagationContext.DynamicSamplingContext + if !dsc.HasEntries() && client != nil { + dsc = DynamicSamplingContextFromScope(scope, client) + } + event.sdkMetaData.dsc = dsc + } + } + if len(scope.extra) > 0 { if event.Extra == nil { event.Extra = make(map[string]interface{}, len(scope.extra)) diff --git a/scope_concurrency_test.go b/scope_concurrency_test.go index ec9a313d..3f2312b9 100644 --- a/scope_concurrency_test.go +++ b/scope_concurrency_test.go @@ -57,6 +57,8 @@ func touchScope(scope *sentry.Scope, x int) { scope.AddAttachment(&sentry.Attachment{Filename: "foo.txt"}) scope.SetUser(sentry.User{ID: "foo"}) scope.SetRequest(httptest.NewRequest("GET", "/foo", nil)) + scope.SetPropagationContext(sentry.NewPropagationContext()) + scope.SetSpan(&sentry.Span{TraceID: sentry.TraceIDFromHex("d49d9bf66f13450b81f65bc51cf49c03")}) sentry.CaptureException(fmt.Errorf("error %d", x)) diff --git a/scope_test.go b/scope_test.go index c855a22e..b056f5fb 100644 --- a/scope_test.go +++ b/scope_test.go @@ -29,6 +29,7 @@ func fillScopeWithData(scope *Scope) *Scope { scope.fingerprint = []string{"scopeFingerprintOne", "scopeFingerprintTwo"} scope.level = LevelDebug scope.request = httptest.NewRequest("GET", "/wat", nil) + scope.propagationContext = NewPropagationContext() return scope } @@ -416,6 +417,10 @@ func TestScopeParentChangedInheritance(t *testing.T) { clone.SetUser(User{ID: "foo"}) r1 := httptest.NewRequest("GET", "/foo", nil) clone.SetRequest(r1) + p1 := NewPropagationContext() + clone.SetPropagationContext(p1) + s1 := &Span{TraceID: TraceIDFromHex("bc6d53f15eb88f4320054569b8c553d4")} + clone.SetSpan(s1) scope.SetTag("foo", "baz") scope.SetContext("foo", Context{"foo": "baz"}) @@ -427,6 +432,10 @@ func TestScopeParentChangedInheritance(t *testing.T) { scope.SetUser(User{ID: "bar"}) r2 := httptest.NewRequest("GET", "/bar", nil) scope.SetRequest(r2) + p2 := NewPropagationContext() + scope.SetPropagationContext(p2) + s2 := &Span{TraceID: TraceIDFromHex("d49d9bf66f13450b81f65bc51cf49c03")} + scope.SetSpan(s2) assertEqual(t, map[string]string{"foo": "bar"}, clone.tags) assertEqual(t, map[string]Context{"foo": {"foo": "bar"}}, clone.contexts) @@ -437,6 +446,8 @@ func TestScopeParentChangedInheritance(t *testing.T) { assertEqual(t, []*Attachment{{Filename: "foo.txt", Payload: []byte("foo")}}, clone.attachments) assertEqual(t, User{ID: "foo"}, clone.user) assertEqual(t, r1, clone.request) + assertEqual(t, p1, clone.propagationContext) + assertEqual(t, s1, clone.span) assertEqual(t, map[string]string{"foo": "baz"}, scope.tags) assertEqual(t, map[string]Context{"foo": {"foo": "baz"}}, scope.contexts) @@ -447,6 +458,7 @@ func TestScopeParentChangedInheritance(t *testing.T) { assertEqual(t, []*Attachment{{Filename: "bar.txt", Payload: []byte("bar")}}, scope.attachments) assertEqual(t, User{ID: "bar"}, scope.user) assertEqual(t, r2, scope.request) + assertEqual(t, s2, scope.span) } func TestScopeChildOverrideInheritance(t *testing.T) { @@ -465,6 +477,10 @@ func TestScopeChildOverrideInheritance(t *testing.T) { scope.AddEventProcessor(func(event *Event, hint *EventHint) *Event { return event }) + p1 := NewPropagationContext() + scope.SetPropagationContext(p1) + s1 := &Span{TraceID: TraceIDFromHex("bc6d53f15eb88f4320054569b8c553d4")} + scope.SetSpan(s1) clone := scope.Clone() clone.SetTag("foo", "bar") @@ -480,6 +496,10 @@ func TestScopeChildOverrideInheritance(t *testing.T) { clone.AddEventProcessor(func(event *Event, hint *EventHint) *Event { return event }) + p2 := NewPropagationContext() + clone.SetPropagationContext(p2) + s2 := &Span{TraceID: TraceIDFromHex("d49d9bf66f13450b81f65bc51cf49c03")} + clone.SetSpan(s2) assertEqual(t, map[string]string{"foo": "bar"}, clone.tags) assertEqual(t, map[string]Context{"foo": {"foo": "bar"}}, clone.contexts) @@ -496,6 +516,8 @@ func TestScopeChildOverrideInheritance(t *testing.T) { }, clone.attachments) assertEqual(t, User{ID: "foo"}, clone.user) assertEqual(t, r2, clone.request) + assertEqual(t, p2, clone.propagationContext) + assertEqual(t, s2, clone.span) assertEqual(t, map[string]string{"foo": "baz"}, scope.tags) assertEqual(t, map[string]Context{"foo": {"foo": "baz"}}, scope.contexts) @@ -506,6 +528,8 @@ func TestScopeChildOverrideInheritance(t *testing.T) { assertEqual(t, []*Attachment{{Filename: "bar.txt", Payload: []byte("bar")}}, scope.attachments) assertEqual(t, User{ID: "bar"}, scope.user) assertEqual(t, r1, scope.request) + assertEqual(t, p1, scope.propagationContext) + assertEqual(t, s1, scope.span) assertEqual(t, len(scope.eventProcessors), 1) assertEqual(t, len(clone.eventProcessors), 2) @@ -524,6 +548,7 @@ func TestClear(t *testing.T) { assertEqual(t, []string{}, scope.fingerprint) assertEqual(t, Level(""), scope.level) assertEqual(t, (*http.Request)(nil), scope.request) + assertEqual(t, (*Span)(nil), scope.span) } func TestClearAndReconfigure(t *testing.T) { @@ -540,6 +565,10 @@ func TestClearAndReconfigure(t *testing.T) { scope.SetUser(User{ID: "foo"}) r := httptest.NewRequest("GET", "/foo", nil) scope.SetRequest(r) + p := NewPropagationContext() + scope.SetPropagationContext(p) + s := &Span{TraceID: TraceIDFromHex("bc6d53f15eb88f4320054569b8c553d4")} + scope.SetSpan(s) assertEqual(t, map[string]string{"foo": "bar"}, scope.tags) assertEqual(t, map[string]Context{"foo": {"foo": "bar"}}, scope.contexts) @@ -550,6 +579,8 @@ func TestClearAndReconfigure(t *testing.T) { assertEqual(t, []*Attachment{{Filename: "foo.txt", Payload: []byte("foo")}}, scope.attachments) assertEqual(t, User{ID: "foo"}, scope.user) assertEqual(t, r, scope.request) + assertEqual(t, p, scope.propagationContext) + assertEqual(t, s, scope.span) } func TestClearBreadcrumbs(t *testing.T) { @@ -570,47 +601,48 @@ func TestApplyToEventWithCorrectScopeAndEvent(t *testing.T) { scope := fillScopeWithData(NewScope()) event := fillEventWithData(NewEvent()) - processedEvent := scope.ApplyToEvent(event, nil) + processedEvent := scope.ApplyToEvent(event, nil, nil) - assertEqual(t, len(processedEvent.Breadcrumbs), 2, "should merge breadcrumbs") - assertEqual(t, len(processedEvent.Attachments), 2, "should merge attachments") - assertEqual(t, len(processedEvent.Tags), 2, "should merge tags") - assertEqual(t, len(processedEvent.Contexts), 3, "should merge contexts") - assertEqual(t, event.Contexts[sharedContextsKey], event.Contexts[sharedContextsKey], "should not override event context") - assertEqual(t, len(processedEvent.Extra), 2, "should merge extra") - assertEqual(t, processedEvent.Level, scope.level, "should use scope level if its set") - assertNotEqual(t, processedEvent.User, scope.user, "should use event user if one exist") - assertNotEqual(t, processedEvent.Request, scope.request, "should use event request if one exist") - assertNotEqual(t, processedEvent.Fingerprint, scope.fingerprint, "should use event fingerprints if they exist") + assertEqual(t, 2, len(processedEvent.Breadcrumbs), "should merge breadcrumbs") + assertEqual(t, 2, len(processedEvent.Attachments), "should merge attachments") + assertEqual(t, 2, len(processedEvent.Tags), "should merge tags") + assertEqual(t, 4, len(processedEvent.Contexts), "should merge contexts") + assertEqual(t, event.Contexts[sharedContextsKey], processedEvent.Contexts[sharedContextsKey], "should not override event trace context") + assertEqual(t, 2, len(processedEvent.Extra), "should merge extra") + assertEqual(t, LevelDebug, processedEvent.Level, "should use event level if set") + assertEqual(t, event.User, processedEvent.User, "should use event user if one exists") + assertEqual(t, event.Request, processedEvent.Request, "should use event request if one exists") + assertEqual(t, event.Fingerprint, processedEvent.Fingerprint, "should use event fingerprints if they exist") + assertNotEqual(t, scope.user, processedEvent.User, "should not use scope user if event user exists") + assertNotEqual(t, scope.request, processedEvent.Request, "should not use scope request if event request exists") + assertNotEqual(t, scope.fingerprint, processedEvent.Fingerprint, "should not use scope fingerprint if event fingerprint exists") } func TestApplyToEventUsingEmptyScope(t *testing.T) { scope := NewScope() event := fillEventWithData(NewEvent()) - processedEvent := scope.ApplyToEvent(event, nil) - + processedEvent := scope.ApplyToEvent(event, nil, nil) assertEqual(t, len(processedEvent.Breadcrumbs), 1, "should use event breadcrumbs") assertEqual(t, len(processedEvent.Attachments), 1, "should use event attachments") assertEqual(t, len(processedEvent.Tags), 1, "should use event tags") - assertEqual(t, len(processedEvent.Contexts), 2, "should use event contexts") + assertEqual(t, len(processedEvent.Contexts), 3, "should use event contexts") assertEqual(t, len(processedEvent.Extra), 1, "should use event extra") - assertNotEqual(t, processedEvent.User, scope.user, "should use event user") - assertNotEqual(t, processedEvent.Fingerprint, scope.fingerprint, "should use event fingerprint") - assertNotEqual(t, processedEvent.Level, scope.level, "should use event level") - assertNotEqual(t, processedEvent.Request, scope.request, "should use event request") + assertEqual(t, processedEvent.User, event.User, "should use event user") + assertEqual(t, processedEvent.Fingerprint, event.Fingerprint, "should use event fingerprint") + assertEqual(t, processedEvent.Level, event.Level, "should use event level") + assertEqual(t, processedEvent.Request, event.Request, "should use event request") } func TestApplyToEventUsingEmptyEvent(t *testing.T) { scope := fillScopeWithData(NewScope()) event := NewEvent() - processedEvent := scope.ApplyToEvent(event, nil) - + processedEvent := scope.ApplyToEvent(event, nil, nil) assertEqual(t, len(processedEvent.Breadcrumbs), 1, "should use scope breadcrumbs") assertEqual(t, len(processedEvent.Attachments), 1, "should use scope attachments") assertEqual(t, len(processedEvent.Tags), 1, "should use scope tags") - assertEqual(t, len(processedEvent.Contexts), 2, "should use scope contexts") + assertEqual(t, len(processedEvent.Contexts), 3, "should use scope contexts") assertEqual(t, len(processedEvent.Extra), 1, "should use scope extra") assertEqual(t, processedEvent.User, scope.user, "should use scope user") assertEqual(t, processedEvent.Fingerprint, scope.fingerprint, "should use scope fingerprint") @@ -631,7 +663,7 @@ func TestEventProcessorsModifiesEvent(t *testing.T) { return event }, } - processedEvent := scope.ApplyToEvent(event, nil) + processedEvent := scope.ApplyToEvent(event, nil, nil) if processedEvent == nil { t.Fatal("event should not be dropped") @@ -648,7 +680,7 @@ func TestEventProcessorsCanDropEvent(t *testing.T) { return nil }, } - processedEvent := scope.ApplyToEvent(event, nil) + processedEvent := scope.ApplyToEvent(event, nil, nil) if processedEvent != nil { t.Error("event should be dropped") @@ -658,7 +690,7 @@ func TestEventProcessorsCanDropEvent(t *testing.T) { func TestEventProcessorsAddEventProcessor(t *testing.T) { scope := NewScope() event := NewEvent() - processedEvent := scope.ApplyToEvent(event, nil) + processedEvent := scope.ApplyToEvent(event, nil, nil) if processedEvent == nil { t.Error("event should not be dropped") @@ -667,7 +699,7 @@ func TestEventProcessorsAddEventProcessor(t *testing.T) { scope.AddEventProcessor(func(event *Event, hint *EventHint) *Event { return nil }) - processedEvent = scope.ApplyToEvent(event, nil) + processedEvent = scope.ApplyToEvent(event, nil, nil) if processedEvent != nil { t.Error("event should be dropped") @@ -695,3 +727,19 @@ func TestCloneContext(t *testing.T) { t.Error("complex values are not supposed to be copied") } } + +func TestScopeSetPropagationContext(t *testing.T) { + scope := NewScope() + p := NewPropagationContext() + scope.SetPropagationContext(p) + + assertEqual(t, scope.propagationContext, p) +} + +func TestScopeSetSpan(t *testing.T) { + scope := NewScope() + s := &Span{TraceID: TraceIDFromHex("bc6d53f15eb88f4320054569b8c553d4")} + scope.SetSpan(s) + + assertEqual(t, scope.span, s) +} diff --git a/tracing.go b/tracing.go index 7d051889..9c585287 100644 --- a/tracing.go +++ b/tracing.go @@ -893,6 +893,22 @@ func WithSpanOrigin(origin SpanOrigin) SpanOption { } } +func GetTraceHeader(s *Scope) string { + if s.span != nil { + return s.span.ToSentryTrace() + } + + return fmt.Sprintf("%s-%s", s.propagationContext.TraceID, s.propagationContext.SpanID) +} + +func GetBaggageHeader(s *Scope) string { + if s.span != nil { + return s.span.ToBaggage() + } + + return s.propagationContext.DynamicSamplingContext.String() +} + // ContinueFromRequest returns a span option that updates the span to continue // an existing trace. If it cannot detect an existing trace in the request, the // span will be left unchanged. diff --git a/tracing_test.go b/tracing_test.go index 795c33d3..da298028 100644 --- a/tracing_test.go +++ b/tracing_test.go @@ -10,6 +10,7 @@ import ( "math" "net/http" "reflect" + "strings" "sync" "testing" "time" @@ -482,6 +483,77 @@ func TestToSentryTrace(t *testing.T) { } } +func TestGetTraceHeader(t *testing.T) { + tests := map[string]struct { + scope *Scope + expected string + }{ + "With span": { + scope: func() *Scope { + s := NewScope() + s.span = &Span{ + TraceID: TraceIDFromHex("d49d9bf66f13450b81f65bc51cf49c03"), + SpanID: SpanIDFromHex("a9f442f9330b4e09"), + Sampled: SampledTrue, + } + return s + }(), + expected: "d49d9bf66f13450b81f65bc51cf49c03-a9f442f9330b4e09-1", + }, + "Without span": { + scope: func() *Scope { + s := NewScope() + s.propagationContext.TraceID = TraceIDFromHex("d49d9bf66f13450b81f65bc51cf49c03") + s.propagationContext.SpanID = SpanIDFromHex("a9f442f9330b4e09") + return s + }(), + expected: "d49d9bf66f13450b81f65bc51cf49c03-a9f442f9330b4e09", + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + result := GetTraceHeader(tt.scope) + assertEqual(t, tt.expected, result) + }) + } +} + +func TestGetBaggageHeader(t *testing.T) { + tests := map[string]struct { + scope *Scope + expected string + }{ + "With span": { + scope: func() *Scope { + s := NewScope() + s.span = &Span{} + return s + }(), + expected: "", + }, + "Without span": { + scope: func() *Scope { + s := NewScope() + s.propagationContext.DynamicSamplingContext = DynamicSamplingContext{ + Entries: map[string]string{"release": "1.0.0", "environment": "production"}, + } + return s + }(), + expected: "sentry-environment=production,sentry-release=1.0.0", + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + result := GetBaggageHeader(tt.scope) + res := strings.Split(result, ",") + sortSlice(res) + assertEqual(t, tt.expected, strings.Join(res, ",")) + }) + } +} + func TestContinueSpanFromRequest(t *testing.T) { traceID := TraceIDFromHex("bc6d53f15eb88f4320054569b8c553d4") spanID := SpanIDFromHex("b72fa28504b07285")