diff --git a/http/sentryhttp.go b/http/sentryhttp.go index a41b04a9..86c5df38 100644 --- a/http/sentryhttp.go +++ b/http/sentryhttp.go @@ -86,15 +86,21 @@ func (h *Handler) handle(handler http.Handler) http.HandlerFunc { hub = sentry.CurrentHub().Clone() ctx = sentry.SetHubOnContext(ctx, hub) } - span := sentry.StartSpan(ctx, "http.server", - sentry.TransactionName(fmt.Sprintf("%s %s", r.Method, r.URL.Path)), + options := []sentry.SpanOption{ + sentry.OpName("http.server"), sentry.ContinueFromRequest(r), + } + // We don't mind getting an existing transaction back so we don't need to + // check if it is. + transaction := sentry.StartTransaction(ctx, + fmt.Sprintf("%s %s", r.Method, r.URL.Path), + options..., ) - defer span.Finish() + defer transaction.Finish() // TODO(tracing): if the next handler.ServeHTTP panics, store // information on the transaction accordingly (status, tag, // level?, ...). - r = r.WithContext(span.Context()) + r = r.WithContext(transaction.Context()) hub.Scope().SetRequest(r) defer h.recoverWithSentry(hub, r) // TODO(tracing): use custom response writer to intercept diff --git a/tracing.go b/tracing.go index 6c992fa3..77666d4b 100644 --- a/tracing.go +++ b/tracing.go @@ -567,6 +567,13 @@ func TransactionName(name string) SpanOption { } } +// OpName sets the operation name for a given span. +func OpName(name string) SpanOption { + return func(s *Span) { + s.Op = name + } +} + // 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. @@ -626,3 +633,23 @@ func spanFromContext(ctx context.Context) *Span { } return nil } + +// StartTransaction will create a transaction (root span) if there's no existing +// transaction in the context otherwise, it will return the existing transaction. +func StartTransaction(ctx context.Context, name string, options ...SpanOption) *Span { + currentTransaction, exists := ctx.Value(spanContextKey{}).(*Span) + if exists { + return currentTransaction + } + hub := GetHubFromContext(ctx) + if hub == nil { + hub = CurrentHub().Clone() + ctx = SetHubOnContext(ctx, hub) + } + options = append(options, TransactionName(name)) + return StartSpan( + ctx, + "", + options..., + ) +} diff --git a/tracing_test.go b/tracing_test.go index 69819da7..347a6dde 100644 --- a/tracing_test.go +++ b/tracing_test.go @@ -226,6 +226,78 @@ func TestStartChild(t *testing.T) { } } +func TestStartTransaction(t *testing.T) { + transport := &TransportMock{} + ctx := NewTestContext(ClientOptions{ + Transport: transport, + }) + transactionName := "Test Transaction" + description := "A Description" + status := SpanStatusOK + sampled := SampledTrue + startTime := time.Now() + endTime := startTime.Add(3 * time.Second) + data := map[string]interface{}{ + "k": "v", + } + transaction := StartTransaction(ctx, + transactionName, + func(s *Span) { + s.Description = description + s.Status = status + s.Sampled = sampled + s.StartTime = startTime + s.EndTime = endTime + s.Data = data + }, + ) + transaction.Finish() + + SpanCheck{ + Sampled: sampled, + RecorderLen: 1, + }.Check(t, transaction) + + events := transport.Events() + if got := len(events); got != 1 { + t.Fatalf("sent %d events, want 1", got) + } + want := &Event{ + Type: transactionType, + Transaction: transactionName, + Contexts: map[string]Context{ + "trace": TraceContext{ + TraceID: transaction.TraceID, + SpanID: transaction.SpanID, + Description: description, + Status: status, + }.Map(), + }, + Tags: nil, + // TODO(tracing): the root span / transaction data field is + // mapped into Event.Extra for now, pending spec clarification. + // https://github.com/getsentry/develop/issues/244#issuecomment-778694182 + Extra: transaction.Data, + Timestamp: endTime, + StartTime: startTime, + } + opts := cmp.Options{ + cmpopts.IgnoreFields(Event{}, + "Contexts", "EventID", "Level", "Platform", + "Release", "Sdk", "ServerName", "Modules", + ), + cmpopts.EquateEmpty(), + } + if diff := cmp.Diff(want, events[0], opts); diff != "" { + t.Fatalf("Event mismatch (-want +got):\n%s", diff) + } + // Check trace context explicitly, as we ignored all contexts above to + // disregard other contexts. + if diff := cmp.Diff(want.Contexts["trace"], events[0].Contexts["trace"]); diff != "" { + t.Fatalf("TraceContext mismatch (-want +got):\n%s", diff) + } +} + // testContextKey is used to store a value in a context so that we can check // that SDK operations on that context preserve the original context values. type testContextKey struct{}