diff --git a/dynamic_sampling_context.go b/dynamic_sampling_context.go index fe0d5c46a..f925b9b01 100644 --- a/dynamic_sampling_context.go +++ b/dynamic_sampling_context.go @@ -117,9 +117,8 @@ func (d DynamicSamplingContext) String() string { return baggage.String() } -// DynamicSamplingContextFromScope 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. +// DynamicSamplingContextFromScope constructs a new DynamicSamplingContext +// using a scope and client. func DynamicSamplingContextFromScope(scope *Scope, client Client) DynamicSamplingContext { entries := map[string]string{} client = normalizeClient(client) @@ -131,7 +130,15 @@ func DynamicSamplingContextFromScope(scope *Scope, client Client) DynamicSamplin } } - propagationContext := scope.propagationContext + return dynamicSamplingContextFromPropagationContext(scope.propagationContextSnapshot(), client) +} + +func dynamicSamplingContextFromPropagationContext(propagationContext PropagationContext, client Client) DynamicSamplingContext { + entries := map[string]string{} + client = normalizeClient(client) + if !client.IsEnabled() { + return DynamicSamplingContext{Entries: entries, Frozen: false} + } if traceID := propagationContext.TraceID.String(); traceID != "" { entries["trace_id"] = traceID diff --git a/scope.go b/scope.go index 45263b4df..0c7978e3b 100644 --- a/scope.go +++ b/scope.go @@ -342,84 +342,148 @@ func (scope *Scope) AddEventProcessor(processor EventProcessor) { scope.eventProcessors = append(scope.eventProcessors, processor) } -// ApplyToEvent takes the data from the current scope and attaches it to the event. -func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint, client Client) *Event { //nolint:gocyclo - client = normalizeClient(client) +type scopeSnapshot struct { + scopeData + eventProcessors []EventProcessor +} - scope.mu.RLock() - defer scope.mu.RUnlock() +// snapshotScopes copies and merges scopes from least to most specific. +func snapshotScopes(client Client, scopes ...*Scope) scopeSnapshot { + var snapshot scopeSnapshot + for _, scope := range scopes { + if scope == nil { + continue + } + scope.mu.RLock() + snapshot.mergeLocked(scope) + scope.mu.RUnlock() + } - if len(scope.breadcrumbs) > 0 { - event.Breadcrumbs = append(event.Breadcrumbs, scope.breadcrumbs...) + limit := normalizeClient(client).clientOptions().MaxBreadcrumbs + switch { + case limit < 0: + snapshot.breadcrumbs = nil + case limit == 0: + limit = defaultMaxBreadcrumbs + } + if limit >= 0 && len(snapshot.breadcrumbs) > limit { + snapshot.breadcrumbs = snapshot.breadcrumbs[len(snapshot.breadcrumbs)-limit:] + } + + return snapshot +} + +// mergeLocked copies scope data into snapshot. The caller must hold scope.mu +// for reading. It does not invoke user callbacks. +func (snapshot *scopeSnapshot) mergeLocked(scope *Scope) { + snapshot.breadcrumbs = append(snapshot.breadcrumbs, scope.breadcrumbs...) + snapshot.attachments = append(snapshot.attachments, scope.attachments...) + snapshot.eventProcessors = append(snapshot.eventProcessors, scope.eventProcessors...) + + if snapshot.attributes == nil { + snapshot.attributes = make(map[string]attribute.Value, len(scope.attributes)) + } + for key, value := range scope.attributes { + snapshot.attributes[key] = value + } + if snapshot.tags == nil { + snapshot.tags = make(map[string]string, len(scope.tags)) + } + for key, value := range scope.tags { + snapshot.tags[key] = value + } + if snapshot.contexts == nil { + snapshot.contexts = make(map[string]Context, len(scope.contexts)) + } + for key, value := range scope.contexts { + snapshot.contexts[key] = value + } + + if !scope.user.IsEmpty() { + snapshot.user = scope.user + } + if len(scope.fingerprint) > 0 { + snapshot.fingerprint = append(snapshot.fingerprint[:0], scope.fingerprint...) + } + if scope.level != "" { + snapshot.level = scope.level + } + if scope.request != nil { + snapshot.request = scope.request + snapshot.requestBody = scope.requestBody } - if len(scope.attachments) > 0 { - event.Attachments = append(event.Attachments, scope.attachments...) + snapshot.propagationContext = scope.propagationContext + if scope.span != nil { + snapshot.span = scope.span } +} + +// ApplyToEvent takes the data from the current scope and attaches it to the event. +func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint, client Client) *Event { + return snapshotScopes(client, scope).applyToEvent(event, hint, client) +} + +func (snapshot scopeSnapshot) applyToEvent(event *Event, hint *EventHint, client Client) *Event { //nolint:gocyclo + client = normalizeClient(client) - if len(scope.tags) > 0 { + if len(snapshot.breadcrumbs) > 0 { + event.Breadcrumbs = append(event.Breadcrumbs, snapshot.breadcrumbs...) + } + if len(snapshot.attachments) > 0 { + event.Attachments = append(event.Attachments, snapshot.attachments...) + } + if len(snapshot.tags) > 0 { if event.Tags == nil { - event.Tags = make(map[string]string, len(scope.tags)) + event.Tags = make(map[string]string, len(snapshot.tags)) } - - for key, value := range scope.tags { - event.Tags[key] = value + for key, value := range snapshot.tags { + if _, ok := event.Tags[key]; !ok { + event.Tags[key] = value + } } } - - if len(scope.contexts) > 0 { + if len(snapshot.contexts) > 0 { if event.Contexts == nil { event.Contexts = make(map[string]Context) } - - for key, value := range scope.contexts { + for key, value := range snapshot.contexts { if key == "trace" && event.Type == transactionType { - // Do not override trace context of - // transactions, otherwise it breaks the - // transaction event representation. - // For error events, the trace context is used - // to link errors and traces/spans in Sentry. continue } - - // Ensure we are not overwriting event fields if _, ok := event.Contexts[key]; !ok { event.Contexts[key] = cloneContext(value) } } } - if event.Contexts == nil { event.Contexts = make(map[string]Context) } - if scope.span != nil { + if snapshot.span != nil { if _, ok := event.Contexts["trace"]; !ok { - event.Contexts["trace"] = scope.span.traceContext().Map() + event.Contexts["trace"] = snapshot.span.traceContext().Map() } - - transaction := scope.span.GetTransaction() - if transaction != nil { + if transaction := snapshot.span.GetTransaction(); transaction != nil { event.sdkMetaData.dsc = DynamicSamplingContextFromTransaction(transaction) } } else { - event.Contexts["trace"] = scope.propagationContext.Map() - - dsc := scope.propagationContext.DynamicSamplingContext + if _, ok := event.Contexts["trace"]; !ok { + event.Contexts["trace"] = snapshot.propagationContext.Map() + } + dsc := snapshot.propagationContext.DynamicSamplingContext if !dsc.HasEntries() { - dsc = DynamicSamplingContextFromScope(scope, client) + dsc = dynamicSamplingContextFromPropagationContext(snapshot.propagationContext, client) } event.sdkMetaData.dsc = dsc } - // If an external trace resolver is registered (e.g. OTel), override - // trace/span IDs from the hint context or the scope's request context. var ctx context.Context if hint != nil { ctx = hint.Context } - if ctx == nil && scope.request != nil { - ctx = scope.request.Context() + if ctx == nil && snapshot.request != nil { + ctx = snapshot.request.Context() } if traceID, spanID, ok := client.externalTraceContextFromContext(ctx); event.Type != transactionType && ok { traceCtx := event.Contexts["trace"] @@ -428,19 +492,16 @@ func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint, client Client) * } if event.User.IsEmpty() { - event.User = scope.user + event.User = snapshot.user } - if len(event.Fingerprint) == 0 { - event.Fingerprint = append(event.Fingerprint, scope.fingerprint...) + event.Fingerprint = append(event.Fingerprint, snapshot.fingerprint...) } - - if scope.level != "" { - event.Level = scope.level + if event.Level == "" { + event.Level = snapshot.level } - - if event.Request == nil && scope.request != nil { - event.Request = newRequest(scope.request, client) + if event.Request == nil && snapshot.request != nil { + event.Request = newRequest(snapshot.request, client) // NOTE: The SDK does not attempt to send partial request body data. // // The reason being that Sentry's ingest pipeline and UI are optimized @@ -451,12 +512,12 @@ func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint, client Client) * // Users can still send more data along their events if they want to, // for example using Event.Contexts. dc := client.GetDataCollection() - if scope.requestBody != nil && !scope.requestBody.Overflow() && dc.CollectHTTPBody(BodyIncomingRequest) { - event.Request.Data = dc.FilterHTTPBody(scope.requestBody.Bytes(), scope.request.Header.Get("Content-Type")) + if snapshot.requestBody != nil && !snapshot.requestBody.Overflow() && dc.CollectHTTPBody(BodyIncomingRequest) { + event.Request.Data = dc.FilterHTTPBody(snapshot.requestBody.Bytes(), snapshot.request.Header.Get("Content-Type")) } } - for _, processor := range scope.eventProcessors { + for _, processor := range snapshot.eventProcessors { id := event.EventID category := event.toCategory() spanCountBefore := event.GetSpanCount() diff --git a/scope_test.go b/scope_test.go index bfc556af2..d176021e0 100644 --- a/scope_test.go +++ b/scope_test.go @@ -24,7 +24,10 @@ func fillScopeWithData(scope *Scope) *Scope { }, } scope.user = User{ID: "1337"} - scope.tags = map[string]string{"scopeTagKey": "scopeTagValue"} + scope.tags = map[string]string{ + "scopeTagKey": "scopeTagValue", + "sharedTagKey": "scopeTagValue", + } scope.contexts = map[string]Context{ "scopeContextsKey": {"scopeContextKey": "scopeContextValue"}, sharedContextsKey: {"scopeContextKey": "scopeContextValue"}, @@ -46,7 +49,10 @@ func fillEventWithData(event *Event) *Event { }, } event.User = User{ID: "42"} - event.Tags = map[string]string{"eventTagKey": "eventTagValue"} + event.Tags = map[string]string{ + "eventTagKey": "eventTagValue", + "sharedTagKey": "eventTagValue", + } event.Contexts = map[string]Context{ "eventContextsKey": {"eventContextKey": "eventContextValue"}, sharedContextsKey: {"eventContextKey": "eventContextKey"}, @@ -610,10 +616,11 @@ func TestApplyToEventWithCorrectScopeAndEvent(t *testing.T) { 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, 3, len(processedEvent.Tags), "should merge tags") + assertEqual(t, "eventTagValue", processedEvent.Tags["sharedTagKey"], "should not override event 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, LevelDebug, processedEvent.Level, "should use event level if set") + assertEqual(t, LevelInfo, 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") @@ -629,7 +636,7 @@ func TestApplyToEventUsingEmptyScope(t *testing.T) { processedEvent := scope.ApplyToEvent(event, nil, noopClient{}) 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.Tags), 2, "should use event tags") assertEqual(t, len(processedEvent.Contexts), 3, "should use event contexts") assertEqual(t, processedEvent.User, event.User, "should use event user") assertEqual(t, processedEvent.Fingerprint, event.Fingerprint, "should use event fingerprint") @@ -644,7 +651,7 @@ func TestApplyToEventUsingEmptyEvent(t *testing.T) { processedEvent := scope.ApplyToEvent(event, nil, noopClient{}) 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.Tags), 2, "should use scope tags") assertEqual(t, len(processedEvent.Contexts), 3, "should use scope contexts") assertEqual(t, processedEvent.User, scope.user, "should use scope user") assertEqual(t, processedEvent.Fingerprint, scope.fingerprint, "should use scope fingerprint")