diff --git a/CHANGELOG.md b/CHANGELOG.md index 33e7b147..ffd7bf26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- ref: Use a `Context` type mapping to a `map[string]interface{}` for all event contexts (#444) + ## v0.13.0 - ref: Change DSN ProjectID to be a string (#420) diff --git a/integrations.go b/integrations.go index c2470496..30ba05c4 100644 --- a/integrations.go +++ b/integrations.go @@ -72,18 +72,18 @@ func (ei *environmentIntegration) SetupOnce(client *Client) { func (ei *environmentIntegration) processor(event *Event, hint *EventHint) *Event { // Initialize maps as necessary. if event.Contexts == nil { - event.Contexts = make(map[string]interface{}) + event.Contexts = make(map[string]Context) } for _, name := range []string{"device", "os", "runtime"} { if event.Contexts[name] == nil { - event.Contexts[name] = make(map[string]interface{}) + event.Contexts[name] = make(Context) } } // Set contextual information preserving existing data. For each context, if // the existing value is not of type map[string]interface{}, then no // additional information is added. - if deviceContext, ok := event.Contexts["device"].(map[string]interface{}); ok { + if deviceContext, ok := event.Contexts["device"]; ok { if _, ok := deviceContext["arch"]; !ok { deviceContext["arch"] = runtime.GOARCH } @@ -91,12 +91,12 @@ func (ei *environmentIntegration) processor(event *Event, hint *EventHint) *Even deviceContext["num_cpu"] = runtime.NumCPU() } } - if osContext, ok := event.Contexts["os"].(map[string]interface{}); ok { + if osContext, ok := event.Contexts["os"]; ok { if _, ok := osContext["name"]; !ok { osContext["name"] = runtime.GOOS } } - if runtimeContext, ok := event.Contexts["runtime"].(map[string]interface{}); ok { + if runtimeContext, ok := event.Contexts["runtime"]; ok { if _, ok := runtimeContext["name"]; !ok { runtimeContext["name"] = "go" } diff --git a/integrations_test.go b/integrations_test.go index 83d2b4a3..389fe301 100644 --- a/integrations_test.go +++ b/integrations_test.go @@ -357,13 +357,13 @@ func TestEnvironmentIntegrationDoesNotOverrideExistingContexts(t *testing.T) { } scope := NewScope() - scope.contexts["device"] = map[string]interface{}{ + scope.contexts["device"] = Context{ "foo": "bar", } - scope.contexts["os"] = map[string]interface{}{ + scope.contexts["os"] = Context{ "name": "test", } - scope.contexts["custom"] = "value" + scope.contexts["custom"] = Context{"key": "value"} hub := NewHub(client, scope) hub.CaptureMessage("test event") @@ -378,13 +378,13 @@ func TestEnvironmentIntegrationDoesNotOverrideExistingContexts(t *testing.T) { contexts := events[0].Contexts - if contexts["device"].(map[string]interface{})["foo"] != "bar" { + if contexts["device"]["foo"] != "bar" { t.Errorf(`contexts["device"] = %#v, want contexts["device"]["foo"] == "bar"`, contexts["device"]) } - if contexts["os"].(map[string]interface{})["name"] != "test" { + if contexts["os"]["name"] != "test" { t.Errorf(`contexts["os"] = %#v, want contexts["os"]["name"] == "test"`, contexts["os"]) } - if contexts["custom"] != "value" { - t.Errorf(`contexts["custom"] = %#v, want "value"`, contexts["custom"]) + if contexts["custom"]["key"] != "value" { + t.Errorf(`contexts["custom"]["key"] = %#v, want "value"`, contexts["custom"]["key"]) } } diff --git a/interfaces.go b/interfaces.go index 1a1b19c6..a8250206 100644 --- a/interfaces.go +++ b/interfaces.go @@ -161,10 +161,12 @@ type Exception struct { // An EventID must be 32 characters long, lowercase and not have any dashes. type EventID string +type Context = map[string]interface{} + // Event is the fundamental data structure that is sent to Sentry. type Event struct { Breadcrumbs []*Breadcrumb `json:"breadcrumbs,omitempty"` - Contexts map[string]interface{} `json:"contexts,omitempty"` + Contexts map[string]Context `json:"contexts,omitempty"` Dist string `json:"dist,omitempty"` Environment string `json:"environment,omitempty"` EventID EventID `json:"event_id,omitempty"` @@ -286,7 +288,7 @@ func (e *Event) transactionMarshalJSON() ([]byte, error) { // NewEvent creates a new Event. func NewEvent() *Event { event := Event{ - Contexts: make(map[string]interface{}), + Contexts: make(map[string]Context), Extra: make(map[string]interface{}), Tags: make(map[string]string), Modules: make(map[string]string), diff --git a/interfaces_test.go b/interfaces_test.go index b564c65c..39eae50e 100644 --- a/interfaces_test.go +++ b/interfaces_test.go @@ -126,8 +126,10 @@ func TestStructSnapshots(t *testing.T) { Extra: map[string]interface{}{ "extra_key": "extra_val", }, - Contexts: map[string]interface{}{ - "context_key": "context_val", + Contexts: map[string]Context{ + "context_key": { + "context_key": "context_val", + }, }, }, }, @@ -138,14 +140,14 @@ func TestStructSnapshots(t *testing.T) { Spans: []*Span{testSpan}, StartTime: time.Unix(3, 0).UTC(), Timestamp: time.Unix(5, 0).UTC(), - Contexts: map[string]interface{}{ - "trace": &TraceContext{ + Contexts: map[string]Context{ + "trace": TraceContext{ TraceID: TraceIDFromHex("90d57511038845dcb4164a70fc3a7fdb"), SpanID: SpanIDFromHex("f7f3fd754a9040eb"), Op: "http.GET", Description: "description", Status: SpanStatusOK, - }, + }.Map(), }, }, }, diff --git a/scope.go b/scope.go index bacd2d70..f53d39d7 100644 --- a/scope.go +++ b/scope.go @@ -28,7 +28,7 @@ type Scope struct { breadcrumbs []*Breadcrumb user User tags map[string]string - contexts map[string]interface{} + contexts map[string]Context extra map[string]interface{} fingerprint []string level Level @@ -51,7 +51,7 @@ func NewScope() *Scope { scope := Scope{ breadcrumbs: make([]*Breadcrumb, 0), tags: make(map[string]string), - contexts: make(map[string]interface{}), + contexts: make(map[string]Context), extra: make(map[string]interface{}), fingerprint: make([]string, 0), } @@ -209,7 +209,7 @@ func (scope *Scope) RemoveTag(key string) { } // SetContext adds a context to the current scope. -func (scope *Scope) SetContext(key string, value interface{}) { +func (scope *Scope) SetContext(key string, value Context) { scope.mu.Lock() defer scope.mu.Unlock() @@ -217,7 +217,7 @@ func (scope *Scope) SetContext(key string, value interface{}) { } // SetContexts assigns multiple contexts to the current scope. -func (scope *Scope) SetContexts(contexts map[string]interface{}) { +func (scope *Scope) SetContexts(contexts map[string]Context) { scope.mu.Lock() defer scope.mu.Unlock() @@ -358,7 +358,7 @@ func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint) *Event { if len(scope.contexts) > 0 { if event.Contexts == nil { - event.Contexts = make(map[string]interface{}) + event.Contexts = make(map[string]Context) } for key, value := range scope.contexts { diff --git a/scope_concurrency_test.go b/scope_concurrency_test.go index b36f7335..e609d44e 100644 --- a/scope_concurrency_test.go +++ b/scope_concurrency_test.go @@ -49,7 +49,7 @@ func TestConcurrentScopeUsage(t *testing.T) { func touchScope(scope *sentry.Scope, x int) { scope.SetTag("foo", "bar") - scope.SetContext("foo", "bar") + scope.SetContext("foo", sentry.Context{"foo": "bar"}) scope.SetExtra("foo", "bar") scope.SetLevel(sentry.LevelDebug) scope.SetTransaction("foo") diff --git a/scope_test.go b/scope_test.go index ae724f5e..b7fb8c39 100644 --- a/scope_test.go +++ b/scope_test.go @@ -13,7 +13,7 @@ func fillScopeWithData(scope *Scope) *Scope { scope.breadcrumbs = []*Breadcrumb{{Timestamp: testNow, Message: "scopeBreadcrumbMessage"}} scope.user = User{ID: "1337"} scope.tags = map[string]string{"scopeTagKey": "scopeTagValue"} - scope.contexts = map[string]interface{}{"scopeContextsKey": "scopeContextsValue"} + scope.contexts = map[string]Context{"scopeContextsKey": {"scopeContextKey": "scopeContextValue"}} scope.extra = map[string]interface{}{"scopeExtraKey": "scopeExtraValue"} scope.fingerprint = []string{"scopeFingerprintOne", "scopeFingerprintTwo"} scope.level = LevelDebug @@ -26,7 +26,7 @@ func fillEventWithData(event *Event) *Event { event.Breadcrumbs = []*Breadcrumb{{Timestamp: testNow, Message: "eventBreadcrumbMessage"}} event.User = User{ID: "42"} event.Tags = map[string]string{"eventTagKey": "eventTagValue"} - event.Contexts = map[string]interface{}{"eventContextsKey": "eventContextsValue"} + event.Contexts = map[string]Context{"eventContextsKey": {"eventContextKey": "eventContextValue"}} event.Extra = map[string]interface{}{"eventExtraKey": "eventExtraValue"} event.Fingerprint = []string{"eventFingerprintOne", "eventFingerprintTwo"} event.Level = LevelInfo @@ -139,72 +139,72 @@ func TestScopeRemoveTagOnEmptyScope(t *testing.T) { func TestScopeSetContext(t *testing.T) { scope := NewScope() - scope.SetContext("a", 1) + scope.SetContext("a", Context{"b": 1}) - assertEqual(t, map[string]interface{}{"a": 1}, scope.contexts) + assertEqual(t, map[string]Context{"a": {"b": 1}}, scope.contexts) } func TestScopeSetContextMerges(t *testing.T) { scope := NewScope() - scope.SetContext("a", "foo") - scope.SetContext("b", 2) + scope.SetContext("a", Context{"foo": "bar"}) + scope.SetContext("b", Context{"b": 2}) - assertEqual(t, map[string]interface{}{"a": "foo", "b": 2}, scope.contexts) + assertEqual(t, map[string]Context{"a": {"foo": "bar"}, "b": {"b": 2}}, scope.contexts) } func TestScopeSetContextOverrides(t *testing.T) { scope := NewScope() - scope.SetContext("a", "foo") - scope.SetContext("a", 2) + scope.SetContext("a", Context{"foo": "bar"}) + scope.SetContext("a", Context{"foo": 2}) - assertEqual(t, map[string]interface{}{"a": 2}, scope.contexts) + assertEqual(t, map[string]Context{"a": {"foo": 2}}, scope.contexts) } func TestScopeSetContexts(t *testing.T) { scope := NewScope() - scope.SetContexts(map[string]interface{}{"a": 1}) + scope.SetContexts(map[string]Context{"a": {"b": 1}}) - assertEqual(t, map[string]interface{}{"a": 1}, scope.contexts) + assertEqual(t, map[string]Context{"a": {"b": 1}}, scope.contexts) } func TestScopeSetContextsMerges(t *testing.T) { scope := NewScope() - scope.SetContexts(map[string]interface{}{"a": "foo"}) - scope.SetContexts(map[string]interface{}{"b": 2, "c": 3}) + scope.SetContexts(map[string]Context{"a": {"a": "foo"}}) + scope.SetContexts(map[string]Context{"b": {"b": 2}, "c": {"c": 3}}) - assertEqual(t, map[string]interface{}{"a": "foo", "b": 2, "c": 3}, scope.contexts) + assertEqual(t, map[string]Context{"a": {"a": "foo"}, "b": {"b": 2}, "c": {"c": 3}}, scope.contexts) } func TestScopeSetContextsOverrides(t *testing.T) { scope := NewScope() - scope.SetContexts(map[string]interface{}{"a": "foo"}) - scope.SetContexts(map[string]interface{}{"a": 2, "b": 3}) + scope.SetContexts(map[string]Context{"a": {"foo": "bar"}}) + scope.SetContexts(map[string]Context{"a": {"a": 2}, "b": {"b": 3}}) - assertEqual(t, map[string]interface{}{"a": 2, "b": 3}, scope.contexts) + assertEqual(t, map[string]Context{"a": {"a": 2}, "b": {"b": 3}}, scope.contexts) } func TestScopeRemoveContext(t *testing.T) { scope := NewScope() - scope.SetContext("a", "foo") - scope.SetContext("b", "bar") + scope.SetContext("a", Context{"foo": "foo"}) + scope.SetContext("b", Context{"bar": "bar"}) scope.RemoveContext("b") - assertEqual(t, map[string]interface{}{"a": "foo"}, scope.contexts) + assertEqual(t, map[string]Context{"a": {"foo": "foo"}}, scope.contexts) } func TestScopeRemoveContextSkipsEmptyValues(t *testing.T) { scope := NewScope() - scope.SetContext("a", "foo") + scope.SetContext("a", Context{"foo": "bar"}) scope.RemoveContext("b") - assertEqual(t, map[string]interface{}{"a": "foo"}, scope.contexts) + assertEqual(t, map[string]Context{"a": {"foo": "bar"}}, scope.contexts) } func TestScopeRemoveContextOnEmptyScope(t *testing.T) { scope := NewScope() scope.RemoveContext("b") - assertEqual(t, make(map[string]interface{}), scope.contexts) + assertEqual(t, make(map[string]Context), scope.contexts) } func TestScopeSetExtra(t *testing.T) { @@ -274,7 +274,7 @@ func TestScopeRemoveExtraOnEmptyScope(t *testing.T) { scope := NewScope() scope.RemoveExtra("b") - assertEqual(t, make(map[string]interface{}), scope.contexts) + assertEqual(t, make(map[string]Context), scope.contexts) } func TestScopeSetFingerprint(t *testing.T) { @@ -383,7 +383,7 @@ func TestScopeParentChangedInheritance(t *testing.T) { clone := scope.Clone() clone.SetTag("foo", "bar") - clone.SetContext("foo", "bar") + clone.SetContext("foo", Context{"foo": "bar"}) clone.SetExtra("foo", "bar") clone.SetLevel(LevelDebug) clone.SetTransaction("foo") @@ -394,7 +394,7 @@ func TestScopeParentChangedInheritance(t *testing.T) { clone.SetRequest(r1) scope.SetTag("foo", "baz") - scope.SetContext("foo", "baz") + scope.SetContext("foo", Context{"foo": "baz"}) scope.SetExtra("foo", "baz") scope.SetLevel(LevelFatal) scope.SetTransaction("bar") @@ -405,7 +405,7 @@ func TestScopeParentChangedInheritance(t *testing.T) { scope.SetRequest(r2) assertEqual(t, map[string]string{"foo": "bar"}, clone.tags) - assertEqual(t, map[string]interface{}{"foo": "bar"}, clone.contexts) + assertEqual(t, map[string]Context{"foo": {"foo": "bar"}}, clone.contexts) assertEqual(t, map[string]interface{}{"foo": "bar"}, clone.extra) assertEqual(t, LevelDebug, clone.level) assertEqual(t, "foo", clone.transaction) @@ -415,7 +415,7 @@ func TestScopeParentChangedInheritance(t *testing.T) { assertEqual(t, r1, clone.request) assertEqual(t, map[string]string{"foo": "baz"}, scope.tags) - assertEqual(t, map[string]interface{}{"foo": "baz"}, scope.contexts) + assertEqual(t, map[string]Context{"foo": {"foo": "baz"}}, scope.contexts) assertEqual(t, map[string]interface{}{"foo": "baz"}, scope.extra) assertEqual(t, LevelFatal, scope.level) assertEqual(t, "bar", scope.transaction) @@ -429,7 +429,7 @@ func TestScopeChildOverrideInheritance(t *testing.T) { scope := NewScope() scope.SetTag("foo", "baz") - scope.SetContext("foo", "baz") + scope.SetContext("foo", Context{"foo": "baz"}) scope.SetExtra("foo", "baz") scope.SetLevel(LevelFatal) scope.SetTransaction("bar") @@ -444,7 +444,7 @@ func TestScopeChildOverrideInheritance(t *testing.T) { clone := scope.Clone() clone.SetTag("foo", "bar") - clone.SetContext("foo", "bar") + clone.SetContext("foo", Context{"foo": "bar"}) clone.SetExtra("foo", "bar") clone.SetLevel(LevelDebug) clone.SetTransaction("foo") @@ -458,7 +458,7 @@ func TestScopeChildOverrideInheritance(t *testing.T) { }) assertEqual(t, map[string]string{"foo": "bar"}, clone.tags) - assertEqual(t, map[string]interface{}{"foo": "bar"}, clone.contexts) + assertEqual(t, map[string]Context{"foo": {"foo": "bar"}}, clone.contexts) assertEqual(t, map[string]interface{}{"foo": "bar"}, clone.extra) assertEqual(t, LevelDebug, clone.level) assertEqual(t, "foo", clone.transaction) @@ -471,7 +471,7 @@ func TestScopeChildOverrideInheritance(t *testing.T) { assertEqual(t, r2, clone.request) assertEqual(t, map[string]string{"foo": "baz"}, scope.tags) - assertEqual(t, map[string]interface{}{"foo": "baz"}, scope.contexts) + assertEqual(t, map[string]Context{"foo": {"foo": "baz"}}, scope.contexts) assertEqual(t, map[string]interface{}{"foo": "baz"}, scope.extra) assertEqual(t, LevelFatal, scope.level) assertEqual(t, "bar", scope.transaction) @@ -491,7 +491,7 @@ func TestClear(t *testing.T) { assertEqual(t, []*Breadcrumb{}, scope.breadcrumbs) assertEqual(t, User{}, scope.user) assertEqual(t, map[string]string{}, scope.tags) - assertEqual(t, map[string]interface{}{}, scope.contexts) + assertEqual(t, map[string]Context{}, scope.contexts) assertEqual(t, map[string]interface{}{}, scope.extra) assertEqual(t, []string{}, scope.fingerprint) assertEqual(t, Level(""), scope.level) @@ -504,7 +504,7 @@ func TestClearAndReconfigure(t *testing.T) { scope.Clear() scope.SetTag("foo", "bar") - scope.SetContext("foo", "bar") + scope.SetContext("foo", Context{"foo": "bar"}) scope.SetExtra("foo", "bar") scope.SetLevel(LevelDebug) scope.SetTransaction("foo") @@ -515,7 +515,7 @@ func TestClearAndReconfigure(t *testing.T) { scope.SetRequest(r) assertEqual(t, map[string]string{"foo": "bar"}, scope.tags) - assertEqual(t, map[string]interface{}{"foo": "bar"}, scope.contexts) + assertEqual(t, map[string]Context{"foo": {"foo": "bar"}}, scope.contexts) assertEqual(t, map[string]interface{}{"foo": "bar"}, scope.extra) assertEqual(t, LevelDebug, scope.level) assertEqual(t, "foo", scope.transaction) diff --git a/testdata/error_event.golden b/testdata/error_event.golden index e40b708c..b778f743 100644 --- a/testdata/error_event.golden +++ b/testdata/error_event.golden @@ -7,7 +7,9 @@ } ], "contexts": { - "context_key": "context_val" + "context_key": { + "context_key": "context_val" + } }, "environment": "production", "event_id": "0123456789abcdef", diff --git a/testdata/transaction_event.golden b/testdata/transaction_event.golden index 7468fdb3..af8545e2 100644 --- a/testdata/transaction_event.golden +++ b/testdata/transaction_event.golden @@ -1,11 +1,11 @@ { "contexts": { "trace": { - "trace_id": "90d57511038845dcb4164a70fc3a7fdb", - "span_id": "f7f3fd754a9040eb", - "op": "http.GET", "description": "description", - "status": "ok" + "op": "http.GET", + "span_id": "f7f3fd754a9040eb", + "status": "ok", + "trace_id": "90d57511038845dcb4164a70fc3a7fdb" } }, "sdk": {}, diff --git a/tracing.go b/tracing.go index 9cabb355..6c992fa3 100644 --- a/tracing.go +++ b/tracing.go @@ -148,7 +148,7 @@ func StartSpan(ctx context.Context, operation string, options ...SpanOption) *Sp // Update scope so that all events include a trace context, allowing // Sentry to correlate errors to transactions/spans. - hubFromContext(ctx).Scope().SetContext("trace", span.traceContext()) + hubFromContext(ctx).Scope().SetContext("trace", span.traceContext().Map()) return &span } @@ -329,8 +329,8 @@ func (s *Span) toEvent() *Event { return &Event{ Type: transactionType, Transaction: hub.Scope().Transaction(), - Contexts: map[string]interface{}{ - "trace": s.traceContext(), + Contexts: map[string]Context{ + "trace": s.traceContext().Map(), }, Tags: s.Tags, Extra: s.Data, @@ -499,6 +499,31 @@ func (tc *TraceContext) MarshalJSON() ([]byte, error) { }) } +func (tc TraceContext) Map() map[string]interface{} { + m := map[string]interface{}{ + "trace_id": tc.TraceID, + "span_id": tc.SpanID, + } + + if tc.ParentSpanID != [8]byte{} { + m["parent_span_id"] = tc.ParentSpanID + } + + if tc.Op != "" { + m["op"] = tc.Op + } + + if tc.Description != "" { + m["description"] = tc.Description + } + + if tc.Status > 0 && tc.Status < maxSpanStatus { + m["status"] = tc.Status + } + + return m +} + // Sampled signifies a sampling decision. type Sampled int8 diff --git a/tracing_test.go b/tracing_test.go index 1769a256..69819da7 100644 --- a/tracing_test.go +++ b/tracing_test.go @@ -130,15 +130,15 @@ func TestStartSpan(t *testing.T) { want := &Event{ Type: transactionType, Transaction: transaction, - Contexts: map[string]interface{}{ - "trace": &TraceContext{ + Contexts: map[string]Context{ + "trace": TraceContext{ TraceID: span.TraceID, SpanID: span.SpanID, ParentSpanID: parentSpanID, Op: op, Description: description, Status: status, - }, + }.Map(), }, Tags: nil, // TODO(tracing): the root span / transaction data field is @@ -190,12 +190,12 @@ func TestStartChild(t *testing.T) { want := &Event{ Type: transactionType, Transaction: "Test Transaction", - Contexts: map[string]interface{}{ - "trace": &TraceContext{ + Contexts: map[string]Context{ + "trace": TraceContext{ TraceID: span.TraceID, SpanID: span.SpanID, Op: span.Op, - }, + }.Map(), }, Spans: []*Span{ { diff --git a/transport_test.go b/transport_test.go index c0d5a1b1..9267a7af 100644 --- a/transport_test.go +++ b/transport_test.go @@ -77,8 +77,8 @@ func TestGetRequestBodyFromEventInvalidExtraField(t *testing.T) { func TestGetRequestBodyFromEventInvalidContextField(t *testing.T) { body := getRequestBodyFromEvent(&Event{ Message: "mkey", - Contexts: map[string]interface{}{ - "wat": unserializableType{}, + Contexts: map[string]Context{ + "wat": {"key": unserializableType{}}, }, }) @@ -101,8 +101,8 @@ func TestGetRequestBodyFromEventMultipleInvalidFields(t *testing.T) { Extra: map[string]interface{}{ "wat": unserializableType{}, }, - Contexts: map[string]interface{}{ - "wat": unserializableType{}, + Contexts: map[string]Context{ + "wat": {"key": unserializableType{}}, }, })