From 6b48923367127dbc7b27d47d111d2ca4a4bac678 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Tue, 27 Aug 2024 09:01:35 +0700 Subject: [PATCH 01/17] feat: http client integration --- httpclient/sentryhttpclient.go | 106 ++++++++++++ httpclient/sentryhttpclient_test.go | 244 ++++++++++++++++++++++++++++ 2 files changed, 350 insertions(+) create mode 100644 httpclient/sentryhttpclient.go create mode 100644 httpclient/sentryhttpclient_test.go diff --git a/httpclient/sentryhttpclient.go b/httpclient/sentryhttpclient.go new file mode 100644 index 000000000..2b28a5153 --- /dev/null +++ b/httpclient/sentryhttpclient.go @@ -0,0 +1,106 @@ +// Package sentryhttpclient provides Sentry integration for Requests modules to enable distributed tracing between services. +// It is compatible with `net/http.RoundTripper`. +// +// import sentryhttpclient "github.com/getsentry/sentry-go/httpclient" +// +// roundTrippper := sentryhttpclient.NewSentryRoundTripper(nil, nil) +// client := &http.Client{ +// Transport: roundTripper, +// } +// +// request, err := client.Do(request) +package sentryhttpclient + +import ( + "fmt" + "net/http" + + "github.com/getsentry/sentry-go" +) + +// SentryRoundTripTracerOption provides a specific type in which defines the option for SentryRoundTripper. +type SentryRoundTripTracerOption func(*SentryRoundTripper) + +// WithTags allows the RoundTripper to includes additional tags. +func WithTags(tags map[string]string) SentryRoundTripTracerOption { + return func(t *SentryRoundTripper) { + for k, v := range tags { + t.tags[k] = v + } + } +} + +// WithTag allows the RoundTripper to includes additional tag. +func WithTag(key, value string) SentryRoundTripTracerOption { + return func(t *SentryRoundTripper) { + t.tags[key] = value + } +} + +// NewSentryRoundTripper provides a wrapper to existing http.RoundTripper to have required span data and trace headers for outgoing HTTP requests. +// +// - If `nil` is passed to `originalRoundTripper`, it will use http.DefaultTransport instead. +func NewSentryRoundTripper(originalRoundTripper http.RoundTripper, opts ...SentryRoundTripTracerOption) http.RoundTripper { + if originalRoundTripper == nil { + originalRoundTripper = http.DefaultTransport + } + + t := &SentryRoundTripper{ + originalRoundTripper: originalRoundTripper, + tags: make(map[string]string), + } + + for _, opt := range opts { + if opt != nil { + opt(t) + } + } + + return t +} + +// SentryRoundTripper provides a http.RoundTripper implementation for Sentry Requests module. +type SentryRoundTripper struct { + originalRoundTripper http.RoundTripper + + tags map[string]string +} + +func (s *SentryRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) { + ctx := request.Context() + cleanRequestURL := request.URL.Redacted() + + span := sentry.StartSpan(ctx, "http.client", sentry.WithTransactionName(fmt.Sprintf("%s %s", request.Method, cleanRequestURL))) + + for k, v := range s.tags { + span.SetTag(k, v) + } + + defer span.Finish() + + span.SetData("http.query", request.URL.Query().Encode()) + span.SetData("http.fragment", request.URL.Fragment) + span.SetData("http.request.method", request.Method) + span.SetData("server.address", request.URL.Hostname()) + span.SetData("server.port", request.URL.Port()) + + // Always add `Baggage` and `Sentry-Trace` headers. + request.Header.Add("Baggage", span.ToBaggage()) + request.Header.Add("Sentry-Trace", span.ToSentryTrace()) + + response, err := s.originalRoundTripper.RoundTrip(request) + + if response != nil { + span.Status = sentry.HTTPtoSpanStatus(response.StatusCode) + span.SetData("http.response.status_code", response.StatusCode) + span.SetData("http.response_content_length", response.ContentLength) + } + + return response, err +} + +// SentryHttpClient provides a default HTTP client with SentryRoundTripper included. +// This can be used directly to perform HTTP request. +var SentryHttpClient = &http.Client{ + Transport: NewSentryRoundTripper(http.DefaultTransport), +} diff --git a/httpclient/sentryhttpclient_test.go b/httpclient/sentryhttpclient_test.go new file mode 100644 index 000000000..e2f43fa31 --- /dev/null +++ b/httpclient/sentryhttpclient_test.go @@ -0,0 +1,244 @@ +package sentryhttpclient_test + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/tls" + "io" + "net/http" + "strconv" + "testing" + + "github.com/getsentry/sentry-go" + sentryhttpclient "github.com/getsentry/sentry-go/httpclient" + "github.com/getsentry/sentry-go/internal/testutils" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +type noopRoundTripper struct { + ExpectResponseStatus int + ExpectResponseLength int +} + +func (n *noopRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) { + responseBody := make([]byte, n.ExpectResponseLength) + rand.Read(responseBody) + return &http.Response{ + Status: "", + StatusCode: n.ExpectResponseStatus, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: map[string][]string{ + "Content-Length": {strconv.Itoa(len(responseBody))}, + }, + Body: io.NopCloser(bytes.NewReader(responseBody)), + ContentLength: int64(len(responseBody)), + TransferEncoding: []string{}, + Close: false, + Uncompressed: false, + Trailer: map[string][]string{}, + Request: request, + TLS: &tls.ConnectionState{}, + }, nil +} + +func TestIntegration(t *testing.T) { + tests := []struct { + RequestMethod string + RequestURL string + TracerOptions []sentryhttpclient.SentryRoundTripTracerOption + WantStatus int + WantResponseLength int + WantTransaction *sentry.Event + }{ + { + RequestMethod: "GET", + RequestURL: "https://example.com/foo", + WantStatus: 200, + WantResponseLength: 0, + WantTransaction: &sentry.Event{ + Extra: map[string]interface{}{ + "http.fragment": string(""), + "http.query": string(""), + "http.request.method": string("GET"), + "http.response.status_code": int(200), + "http.response_content_length": int64(0), + "server.address": string("example.com"), + "server.port": string(""), + }, + Level: sentry.LevelInfo, + Transaction: "GET https://example.com/foo", + Type: "transaction", + TransactionInfo: &sentry.TransactionInfo{Source: "custom"}, + }, + }, + { + RequestMethod: "GET", + RequestURL: "https://example.com:443/foo/bar?baz=123#readme", + TracerOptions: []sentryhttpclient.SentryRoundTripTracerOption{nil, nil, nil}, + WantStatus: 200, + WantResponseLength: 0, + WantTransaction: &sentry.Event{ + Extra: map[string]interface{}{ + "http.fragment": string("readme"), + "http.query": string("baz=123"), + "http.request.method": string("GET"), + "http.response.status_code": int(200), + "http.response_content_length": int64(0), + "server.address": string("example.com"), + "server.port": string("443"), + }, + Level: sentry.LevelInfo, + Transaction: "GET https://example.com:443/foo/bar?baz=123#readme", + Type: "transaction", + TransactionInfo: &sentry.TransactionInfo{Source: "custom"}, + }, + }, + { + RequestMethod: "HEAD", + RequestURL: "https://example.com:8443/foo?bar=123&abc=def", + TracerOptions: []sentryhttpclient.SentryRoundTripTracerOption{sentryhttpclient.WithTag("user", "def"), sentryhttpclient.WithTags(map[string]string{"domain": "example.com"})}, + WantStatus: 400, + WantResponseLength: 0, + WantTransaction: &sentry.Event{ + Extra: map[string]interface{}{ + "http.fragment": string(""), + "http.query": string("abc=def&bar=123"), + "http.request.method": string("HEAD"), + "http.response.status_code": int(400), + "http.response_content_length": int64(0), + "server.address": string("example.com"), + "server.port": string("8443"), + }, + Tags: map[string]string{ + "user": "def", + "domain": "example.com", + }, + Level: sentry.LevelInfo, + Transaction: "HEAD https://example.com:8443/foo?bar=123&abc=def", + Type: "transaction", + TransactionInfo: &sentry.TransactionInfo{Source: "custom"}, + }, + }, + { + RequestMethod: "POST", + RequestURL: "https://john:verysecurepassword@example.com:4321/secret", + WantStatus: 200, + WantResponseLength: 1024, + WantTransaction: &sentry.Event{ + Extra: map[string]interface{}{ + "http.fragment": string(""), + "http.query": string(""), + "http.request.method": string("POST"), + "http.response.status_code": int(200), + "http.response_content_length": int64(1024), + "server.address": string("example.com"), + "server.port": string("4321"), + }, + Level: sentry.LevelInfo, + Transaction: "POST https://john:xxxxx@example.com:4321/secret", + Type: "transaction", + TransactionInfo: &sentry.TransactionInfo{Source: "custom"}, + }, + }, + } + + transactionsCh := make(chan *sentry.Event, len(tests)) + + sentryClient, err := sentry.NewClient(sentry.ClientOptions{ + EnableTracing: true, + TracesSampleRate: 1.0, + BeforeSendTransaction: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { + transactionsCh <- event + return event + }, + }) + if err != nil { + t.Fatal(err) + } + + var want []*sentry.Event + for _, tt := range tests { + hub := sentry.NewHub(sentryClient, sentry.NewScope()) + ctx := sentry.SetHubOnContext(context.Background(), hub) + + request, err := http.NewRequestWithContext(ctx, tt.RequestMethod, tt.RequestURL, nil) + if err != nil { + t.Fatal(err) + } + + roundTripper := &noopRoundTripper{ + ExpectResponseStatus: tt.WantStatus, + ExpectResponseLength: tt.WantResponseLength, + } + + client := &http.Client{ + Transport: sentryhttpclient.NewSentryRoundTripper(roundTripper, tt.TracerOptions...), + } + + response, err := client.Do(request) + if err != nil { + t.Fatal(err) + } + + response.Body.Close() + want = append(want, tt.WantTransaction) + } + + if ok := sentryClient.Flush(testutils.FlushTimeout()); !ok { + t.Fatal("sentry.Flush timed out") + } + close(transactionsCh) + var got []*sentry.Event + for e := range transactionsCh { + got = append(got, e) + } + + optstrans := cmp.Options{ + cmpopts.IgnoreFields( + sentry.Event{}, + "Contexts", "EventID", "Platform", "Modules", + "Release", "Sdk", "ServerName", "Timestamp", + "sdkMetaData", "StartTime", "Spans", + ), + cmpopts.IgnoreFields( + sentry.Request{}, + "Env", + ), + } + if diff := cmp.Diff(want, got, optstrans); diff != "" { + t.Fatalf("Transaction mismatch (-want +got):\n%s", diff) + } +} + +func TestDefaults(t *testing.T) { + t.Run("Create a regular outgoing HTTP request with default NewSentryRoundTripper", func(t *testing.T) { + roundTripper := sentryhttpclient.NewSentryRoundTripper(nil) + client := &http.Client{Transport: roundTripper} + + res, err := client.Head("https://sentry.io") + if err != nil { + t.Error(err) + } + + if res.Body != nil { + res.Body.Close() + } + }) + + t.Run("Create a regular outgoing HTTP request with default SentryHttpClient", func(t *testing.T) { + client := sentryhttpclient.SentryHttpClient + + res, err := client.Head("https://sentry.io") + if err != nil { + t.Error(err) + } + + if res.Body != nil { + res.Body.Close() + } + }) +} From fd66575e1d937d37626a04b9c00579a07eca2c99 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Tue, 27 Aug 2024 09:05:21 +0700 Subject: [PATCH 02/17] chore(httpclient): variable naming according to linter --- httpclient/sentryhttpclient.go | 4 ++-- httpclient/sentryhttpclient_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/httpclient/sentryhttpclient.go b/httpclient/sentryhttpclient.go index 2b28a5153..7ab255d49 100644 --- a/httpclient/sentryhttpclient.go +++ b/httpclient/sentryhttpclient.go @@ -99,8 +99,8 @@ func (s *SentryRoundTripper) RoundTrip(request *http.Request) (*http.Response, e return response, err } -// SentryHttpClient provides a default HTTP client with SentryRoundTripper included. +// SentryHTTPClient provides a default HTTP client with SentryRoundTripper included. // This can be used directly to perform HTTP request. -var SentryHttpClient = &http.Client{ +var SentryHTTPClient = &http.Client{ Transport: NewSentryRoundTripper(http.DefaultTransport), } diff --git a/httpclient/sentryhttpclient_test.go b/httpclient/sentryhttpclient_test.go index e2f43fa31..104a19e45 100644 --- a/httpclient/sentryhttpclient_test.go +++ b/httpclient/sentryhttpclient_test.go @@ -24,7 +24,7 @@ type noopRoundTripper struct { func (n *noopRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) { responseBody := make([]byte, n.ExpectResponseLength) - rand.Read(responseBody) + _, _ = rand.Read(responseBody) return &http.Response{ Status: "", StatusCode: n.ExpectResponseStatus, @@ -230,7 +230,7 @@ func TestDefaults(t *testing.T) { }) t.Run("Create a regular outgoing HTTP request with default SentryHttpClient", func(t *testing.T) { - client := sentryhttpclient.SentryHttpClient + client := sentryhttpclient.SentryHTTPClient res, err := client.Head("https://sentry.io") if err != nil { From 5f4c5ae27d6d69b702af28357ec9497090db2068 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Tue, 3 Sep 2024 20:46:36 +0700 Subject: [PATCH 03/17] chore: add changelog entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f92e704ae..b15c3298b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - Add ability to skip frames in stacktrace ([#852](https://github.com/getsentry/sentry-go/pull/852)) - Remove Martini integration ([#861](https://github.com/getsentry/sentry-go/pull/861)) - Fix closure functions name grouping ([#877](https://github.com/getsentry/sentry-go/pull/877)) - +- Add net/http client integration ([#876](https://github.com/getsentry/sentry-go/pull/876)) ### Breaking Changes From 5214ea5f127f7895fdae8ad8d6208657c3af627c Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Tue, 3 Sep 2024 20:48:58 +0700 Subject: [PATCH 04/17] fix(httpclient): dont iterate over map --- httpclient/sentryhttpclient.go | 6 +----- httpclient/sentryhttpclient_test.go | 3 +++ 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/httpclient/sentryhttpclient.go b/httpclient/sentryhttpclient.go index 7ab255d49..35739ff53 100644 --- a/httpclient/sentryhttpclient.go +++ b/httpclient/sentryhttpclient.go @@ -71,11 +71,7 @@ func (s *SentryRoundTripper) RoundTrip(request *http.Request) (*http.Response, e cleanRequestURL := request.URL.Redacted() span := sentry.StartSpan(ctx, "http.client", sentry.WithTransactionName(fmt.Sprintf("%s %s", request.Method, cleanRequestURL))) - - for k, v := range s.tags { - span.SetTag(k, v) - } - + span.Tags = s.tags defer span.Finish() span.SetData("http.query", request.URL.Query().Encode()) diff --git a/httpclient/sentryhttpclient_test.go b/httpclient/sentryhttpclient_test.go index 104a19e45..96e41d2d3 100644 --- a/httpclient/sentryhttpclient_test.go +++ b/httpclient/sentryhttpclient_test.go @@ -73,6 +73,7 @@ func TestIntegration(t *testing.T) { Transaction: "GET https://example.com/foo", Type: "transaction", TransactionInfo: &sentry.TransactionInfo{Source: "custom"}, + Tags: map[string]string{}, }, }, { @@ -95,6 +96,7 @@ func TestIntegration(t *testing.T) { Transaction: "GET https://example.com:443/foo/bar?baz=123#readme", Type: "transaction", TransactionInfo: &sentry.TransactionInfo{Source: "custom"}, + Tags: map[string]string{}, }, }, { @@ -142,6 +144,7 @@ func TestIntegration(t *testing.T) { Transaction: "POST https://john:xxxxx@example.com:4321/secret", Type: "transaction", TransactionInfo: &sentry.TransactionInfo{Source: "custom"}, + Tags: map[string]string{}, }, }, } From ed105c1893a9cd7b0a9a8edf042616aa2259b80f Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Tue, 3 Sep 2024 21:03:28 +0700 Subject: [PATCH 05/17] feat(examples): add httpclient --- _examples/httpclient/main.go | 75 ++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 _examples/httpclient/main.go diff --git a/_examples/httpclient/main.go b/_examples/httpclient/main.go new file mode 100644 index 000000000..0dfadb343 --- /dev/null +++ b/_examples/httpclient/main.go @@ -0,0 +1,75 @@ +package main + +import ( + "context" + "fmt" + "io" + "net/http" + + "github.com/getsentry/sentry-go" + sentryhttpclient "github.com/getsentry/sentry-go/httpclient" +) + +func main() { + _ = sentry.Init(sentry.ClientOptions{ + Dsn: "", + EnableTracing: true, + TracesSampleRate: 1.0, + BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { + fmt.Println(event) + return event + }, + BeforeSendTransaction: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { + fmt.Println(event) + return event + }, + Debug: true, + }) + + // With custom HTTP client + ctx := sentry.SetHubOnContext(context.Background(), sentry.CurrentHub().Clone()) + httpClient := &http.Client{ + Transport: sentryhttpclient.NewSentryRoundTripper(nil), + } + + err := getExamplePage(ctx, httpClient) + if err != nil { + panic(err) + } + + // With Sentry's HTTP client + err = getExamplePage(ctx, sentryhttpclient.SentryHTTPClient) + if err != nil { + panic(err) + } +} + +func getExamplePage(ctx context.Context, httpClient *http.Client) error { + span := sentry.StartSpan(ctx, "getExamplePage") + ctx = span.Context() + defer span.Finish() + + request, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://example.com", nil) + if err != nil { + return err + } + + response, err := httpClient.Do(request) + if err != nil { + return err + } + defer func() { + if response.Body != nil { + _ = response.Body.Close() + } + }() + + body, err := io.ReadAll(response.Body) + if err != nil { + return err + } + + fmt.Println(string(body)) + + return nil +} From 15aa59a64d9636c15483d91f8d7dce1d73bec8d4 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 4 Sep 2024 08:04:12 +0700 Subject: [PATCH 06/17] feat(spans): only start 'http.client' op as a child span --- httpclient/sentryhttpclient.go | 9 ++- httpclient/sentryhttpclient_test.go | 109 ++++++++++++++++------------ tracing.go | 11 +++ tracing_test.go | 22 ++++++ 4 files changed, 104 insertions(+), 47 deletions(-) diff --git a/httpclient/sentryhttpclient.go b/httpclient/sentryhttpclient.go index 35739ff53..72126dafe 100644 --- a/httpclient/sentryhttpclient.go +++ b/httpclient/sentryhttpclient.go @@ -67,10 +67,15 @@ type SentryRoundTripper struct { } func (s *SentryRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) { - ctx := request.Context() + // Only create the `http.client` span only if there is a parent span. + parentSpan := sentry.GetSpanFromContext(request.Context()) + if parentSpan == nil { + return s.originalRoundTripper.RoundTrip(request) + } + cleanRequestURL := request.URL.Redacted() - span := sentry.StartSpan(ctx, "http.client", sentry.WithTransactionName(fmt.Sprintf("%s %s", request.Method, cleanRequestURL))) + span := parentSpan.StartChild("http.client", sentry.WithTransactionName(fmt.Sprintf("%s %s", request.Method, cleanRequestURL))) span.Tags = s.tags defer span.Finish() diff --git a/httpclient/sentryhttpclient_test.go b/httpclient/sentryhttpclient_test.go index 96e41d2d3..fd99eb826 100644 --- a/httpclient/sentryhttpclient_test.go +++ b/httpclient/sentryhttpclient_test.go @@ -8,6 +8,7 @@ import ( "io" "net/http" "strconv" + "strings" "testing" "github.com/getsentry/sentry-go" @@ -52,15 +53,15 @@ func TestIntegration(t *testing.T) { TracerOptions []sentryhttpclient.SentryRoundTripTracerOption WantStatus int WantResponseLength int - WantTransaction *sentry.Event + WantSpan *sentry.Span }{ { RequestMethod: "GET", RequestURL: "https://example.com/foo", WantStatus: 200, WantResponseLength: 0, - WantTransaction: &sentry.Event{ - Extra: map[string]interface{}{ + WantSpan: &sentry.Span{ + Data: map[string]interface{}{ "http.fragment": string(""), "http.query": string(""), "http.request.method": string("GET"), @@ -69,11 +70,12 @@ func TestIntegration(t *testing.T) { "server.address": string("example.com"), "server.port": string(""), }, - Level: sentry.LevelInfo, - Transaction: "GET https://example.com/foo", - Type: "transaction", - TransactionInfo: &sentry.TransactionInfo{Source: "custom"}, - Tags: map[string]string{}, + Name: "GET https://example.com/foo", + Op: "http.client", + Tags: map[string]string{}, + Origin: "manual", + Sampled: sentry.SampledTrue, + Status: sentry.SpanStatusOK, }, }, { @@ -82,8 +84,8 @@ func TestIntegration(t *testing.T) { TracerOptions: []sentryhttpclient.SentryRoundTripTracerOption{nil, nil, nil}, WantStatus: 200, WantResponseLength: 0, - WantTransaction: &sentry.Event{ - Extra: map[string]interface{}{ + WantSpan: &sentry.Span{ + Data: map[string]interface{}{ "http.fragment": string("readme"), "http.query": string("baz=123"), "http.request.method": string("GET"), @@ -92,11 +94,12 @@ func TestIntegration(t *testing.T) { "server.address": string("example.com"), "server.port": string("443"), }, - Level: sentry.LevelInfo, - Transaction: "GET https://example.com:443/foo/bar?baz=123#readme", - Type: "transaction", - TransactionInfo: &sentry.TransactionInfo{Source: "custom"}, - Tags: map[string]string{}, + Name: "GET https://example.com:443/foo/bar?baz=123#readme", + Op: "http.client", + Tags: map[string]string{}, + Origin: "manual", + Sampled: sentry.SampledTrue, + Status: sentry.SpanStatusOK, }, }, { @@ -105,8 +108,8 @@ func TestIntegration(t *testing.T) { TracerOptions: []sentryhttpclient.SentryRoundTripTracerOption{sentryhttpclient.WithTag("user", "def"), sentryhttpclient.WithTags(map[string]string{"domain": "example.com"})}, WantStatus: 400, WantResponseLength: 0, - WantTransaction: &sentry.Event{ - Extra: map[string]interface{}{ + WantSpan: &sentry.Span{ + Data: map[string]interface{}{ "http.fragment": string(""), "http.query": string("abc=def&bar=123"), "http.request.method": string("HEAD"), @@ -119,10 +122,11 @@ func TestIntegration(t *testing.T) { "user": "def", "domain": "example.com", }, - Level: sentry.LevelInfo, - Transaction: "HEAD https://example.com:8443/foo?bar=123&abc=def", - Type: "transaction", - TransactionInfo: &sentry.TransactionInfo{Source: "custom"}, + Name: "HEAD https://example.com:8443/foo?bar=123&abc=def", + Op: "http.client", + Origin: "manual", + Sampled: sentry.SampledTrue, + Status: sentry.SpanStatusInvalidArgument, }, }, { @@ -130,8 +134,8 @@ func TestIntegration(t *testing.T) { RequestURL: "https://john:verysecurepassword@example.com:4321/secret", WantStatus: 200, WantResponseLength: 1024, - WantTransaction: &sentry.Event{ - Extra: map[string]interface{}{ + WantSpan: &sentry.Span{ + Data: map[string]interface{}{ "http.fragment": string(""), "http.query": string(""), "http.request.method": string("POST"), @@ -140,22 +144,23 @@ func TestIntegration(t *testing.T) { "server.address": string("example.com"), "server.port": string("4321"), }, - Level: sentry.LevelInfo, - Transaction: "POST https://john:xxxxx@example.com:4321/secret", - Type: "transaction", - TransactionInfo: &sentry.TransactionInfo{Source: "custom"}, - Tags: map[string]string{}, + Name: "POST https://john:xxxxx@example.com:4321/secret", + Op: "http.client", + Tags: map[string]string{}, + Origin: "manual", + Sampled: sentry.SampledTrue, + Status: sentry.SpanStatusOK, }, }, } - transactionsCh := make(chan *sentry.Event, len(tests)) + spansCh := make(chan []*sentry.Span, len(tests)) sentryClient, err := sentry.NewClient(sentry.ClientOptions{ EnableTracing: true, TracesSampleRate: 1.0, BeforeSendTransaction: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { - transactionsCh <- event + spansCh <- event.Spans return event }, }) @@ -163,10 +168,11 @@ func TestIntegration(t *testing.T) { t.Fatal(err) } - var want []*sentry.Event for _, tt := range tests { hub := sentry.NewHub(sentryClient, sentry.NewScope()) ctx := sentry.SetHubOnContext(context.Background(), hub) + span := sentry.StartSpan(ctx, "fake_parent", sentry.WithTransactionName("Fake Parent")) + ctx = span.Context() request, err := http.NewRequestWithContext(ctx, tt.RequestMethod, tt.RequestURL, nil) if err != nil { @@ -188,32 +194,45 @@ func TestIntegration(t *testing.T) { } response.Body.Close() - want = append(want, tt.WantTransaction) + span.Finish() } if ok := sentryClient.Flush(testutils.FlushTimeout()); !ok { t.Fatal("sentry.Flush timed out") } - close(transactionsCh) - var got []*sentry.Event - for e := range transactionsCh { + close(spansCh) + + var got [][]*sentry.Span + for e := range spansCh { got = append(got, e) } optstrans := cmp.Options{ cmpopts.IgnoreFields( - sentry.Event{}, - "Contexts", "EventID", "Platform", "Modules", - "Release", "Sdk", "ServerName", "Timestamp", - "sdkMetaData", "StartTime", "Spans", - ), - cmpopts.IgnoreFields( - sentry.Request{}, - "Env", + sentry.Span{}, + "TraceID", "SpanID", "ParentSpanID", "StartTime", "EndTime", + "mu", "parent", "sampleRate", "ctx", "dynamicSamplingContext", "recorder", "finishOnce", "collectProfile", "contexts", ), } - if diff := cmp.Diff(want, got, optstrans); diff != "" { - t.Fatalf("Transaction mismatch (-want +got):\n%s", diff) + for i, tt := range tests { + var foundMatch = false + gotSpans := got[i] + + var diffs []string + for _, gotSpan := range gotSpans { + if diff := cmp.Diff(tt.WantSpan, gotSpan, optstrans); diff != "" { + diffs = append(diffs, diff) + } else { + foundMatch = true + break + } + } + + if foundMatch { + continue + } else { + t.Errorf("Span mismatch (-want +got):\n%s", strings.Join(diffs, "\n")) + } } } diff --git a/tracing.go b/tracing.go index 9c5852872..1f8a8bbd7 100644 --- a/tracing.go +++ b/tracing.go @@ -206,6 +206,17 @@ func StartSpan(ctx context.Context, operation string, options ...SpanOption) *Sp return &span } +// GetSpanFromContext retrieves attached *sentry.Span instance from context.Context. +// If there are no spans, it will return nil. +func GetSpanFromContext(ctx context.Context) *Span { + span, ok := ctx.Value(spanContextKey{}).(*Span) + if ok { + return span + } + + return nil +} + // Finish sets the span's end time, unless already set. If the span is the root // of a span tree, Finish sends the span tree to Sentry as a transaction. // diff --git a/tracing_test.go b/tracing_test.go index da2980286..0cea0975d 100644 --- a/tracing_test.go +++ b/tracing_test.go @@ -1091,3 +1091,25 @@ func TestSpanFinishConcurrentlyWithoutRaces(_ *testing.T) { time.Sleep(50 * time.Millisecond) } + +func TestGetSpanFromContext(t *testing.T) { + t.Run("Exists", func(t *testing.T) { + span := StartSpan(context.Background(), "something") + + value := GetSpanFromContext(span.Context()) + if value == nil { + t.Error("expecting `value` to be not nil") + } else { + if span.Op != "something" { + t.Errorf("expecting `span.Op` to be 'something', instead got %q", span.Op) + } + } + }) + + t.Run("Nil", func(t *testing.T) { + value := GetSpanFromContext(context.Background()) + if value != nil { + t.Error("expecting `value` to be nil") + } + }) +} From 099dad40d7d69ac7ba6abef606488f04c284f288 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 4 Sep 2024 08:06:35 +0700 Subject: [PATCH 07/17] chore: lint --- httpclient/sentryhttpclient_test.go | 4 +--- tracing_test.go | 6 ++---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/httpclient/sentryhttpclient_test.go b/httpclient/sentryhttpclient_test.go index fd99eb826..6bf61b7fb 100644 --- a/httpclient/sentryhttpclient_test.go +++ b/httpclient/sentryhttpclient_test.go @@ -228,9 +228,7 @@ func TestIntegration(t *testing.T) { } } - if foundMatch { - continue - } else { + if !foundMatch { t.Errorf("Span mismatch (-want +got):\n%s", strings.Join(diffs, "\n")) } } diff --git a/tracing_test.go b/tracing_test.go index 0cea0975d..074a8ce43 100644 --- a/tracing_test.go +++ b/tracing_test.go @@ -1099,10 +1099,8 @@ func TestGetSpanFromContext(t *testing.T) { value := GetSpanFromContext(span.Context()) if value == nil { t.Error("expecting `value` to be not nil") - } else { - if span.Op != "something" { - t.Errorf("expecting `span.Op` to be 'something', instead got %q", span.Op) - } + } else if span.Op != "something" { + t.Errorf("expecting `span.Op` to be 'something', instead got %q", span.Op) } }) From f018ddeec53d863adb2805efd56cc92f99d03667 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Fri, 18 Oct 2024 17:23:31 +0700 Subject: [PATCH 08/17] chore: remove GetSpanFromContext and replace it with SpanFromContext --- httpclient/sentryhttpclient.go | 2 +- tracing.go | 11 ----------- tracing_test.go | 20 -------------------- 3 files changed, 1 insertion(+), 32 deletions(-) diff --git a/httpclient/sentryhttpclient.go b/httpclient/sentryhttpclient.go index 72126dafe..60b092a33 100644 --- a/httpclient/sentryhttpclient.go +++ b/httpclient/sentryhttpclient.go @@ -68,7 +68,7 @@ type SentryRoundTripper struct { func (s *SentryRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) { // Only create the `http.client` span only if there is a parent span. - parentSpan := sentry.GetSpanFromContext(request.Context()) + parentSpan := sentry.SpanFromContext(request.Context()) if parentSpan == nil { return s.originalRoundTripper.RoundTrip(request) } diff --git a/tracing.go b/tracing.go index fb1e5acd0..0f5ade2e8 100644 --- a/tracing.go +++ b/tracing.go @@ -206,17 +206,6 @@ func StartSpan(ctx context.Context, operation string, options ...SpanOption) *Sp return &span } -// GetSpanFromContext retrieves attached *sentry.Span instance from context.Context. -// If there are no spans, it will return nil. -func GetSpanFromContext(ctx context.Context) *Span { - span, ok := ctx.Value(spanContextKey{}).(*Span) - if ok { - return span - } - - return nil -} - // Finish sets the span's end time, unless already set. If the span is the root // of a span tree, Finish sends the span tree to Sentry as a transaction. // diff --git a/tracing_test.go b/tracing_test.go index 53225080f..795c33d33 100644 --- a/tracing_test.go +++ b/tracing_test.go @@ -1019,23 +1019,3 @@ func TestSpanFinishConcurrentlyWithoutRaces(_ *testing.T) { time.Sleep(50 * time.Millisecond) } - -func TestGetSpanFromContext(t *testing.T) { - t.Run("Exists", func(t *testing.T) { - span := StartSpan(context.Background(), "something") - - value := GetSpanFromContext(span.Context()) - if value == nil { - t.Error("expecting `value` to be not nil") - } else if span.Op != "something" { - t.Errorf("expecting `span.Op` to be 'something', instead got %q", span.Op) - } - }) - - t.Run("Nil", func(t *testing.T) { - value := GetSpanFromContext(context.Background()) - if value != nil { - t.Error("expecting `value` to be nil") - } - }) -} From 6af10bfe2edd40123bf15b8f9d2989950feb2c86 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Sat, 2 Nov 2024 18:53:45 +0700 Subject: [PATCH 09/17] feat(httpclient): use baggage and traceparent from hub --- httpclient/sentryhttpclient.go | 8 +++ httpclient/sentryhttpclient_test.go | 100 +++++++++++++++++++++++++++- 2 files changed, 105 insertions(+), 3 deletions(-) diff --git a/httpclient/sentryhttpclient.go b/httpclient/sentryhttpclient.go index 60b092a33..167740f99 100644 --- a/httpclient/sentryhttpclient.go +++ b/httpclient/sentryhttpclient.go @@ -70,6 +70,11 @@ func (s *SentryRoundTripper) RoundTrip(request *http.Request) (*http.Response, e // Only create the `http.client` span only if there is a parent span. parentSpan := sentry.SpanFromContext(request.Context()) if parentSpan == nil { + if hub := sentry.GetHubFromContext(request.Context()); hub != nil { + request.Header.Add("Baggage", hub.GetBaggage()) + request.Header.Add("Sentry-Trace", hub.GetTraceparent()) + } + return s.originalRoundTripper.RoundTrip(request) } @@ -90,6 +95,9 @@ func (s *SentryRoundTripper) RoundTrip(request *http.Request) (*http.Response, e request.Header.Add("Sentry-Trace", span.ToSentryTrace()) response, err := s.originalRoundTripper.RoundTrip(request) + if err != nil { + span.Status = sentry.SpanStatusInternalError + } if response != nil { span.Status = sentry.HTTPtoSpanStatus(response.StatusCode) diff --git a/httpclient/sentryhttpclient_test.go b/httpclient/sentryhttpclient_test.go index 6bf61b7fb..0a81fa1e9 100644 --- a/httpclient/sentryhttpclient_test.go +++ b/httpclient/sentryhttpclient_test.go @@ -5,6 +5,7 @@ import ( "context" "crypto/rand" "crypto/tls" + "errors" "io" "net/http" "strconv" @@ -21,9 +22,14 @@ import ( type noopRoundTripper struct { ExpectResponseStatus int ExpectResponseLength int + ExpectError bool } func (n *noopRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) { + if n.ExpectError { + return nil, errors.New("error") + } + responseBody := make([]byte, n.ExpectResponseLength) _, _ = rand.Read(responseBody) return &http.Response{ @@ -53,6 +59,7 @@ func TestIntegration(t *testing.T) { TracerOptions []sentryhttpclient.SentryRoundTripTracerOption WantStatus int WantResponseLength int + WantError bool WantSpan *sentry.Span }{ { @@ -152,6 +159,26 @@ func TestIntegration(t *testing.T) { Status: sentry.SpanStatusOK, }, }, + { + RequestMethod: "POST", + RequestURL: "https://example.com", + WantError: true, + WantSpan: &sentry.Span{ + Data: map[string]interface{}{ + "http.fragment": string(""), + "http.query": string(""), + "http.request.method": string("POST"), + "server.address": string("example.com"), + "server.port": string(""), + }, + Name: "POST https://example.com", + Op: "http.client", + Tags: map[string]string{}, + Origin: "manual", + Sampled: sentry.SampledTrue, + Status: sentry.SpanStatusInternalError, + }, + }, } spansCh := make(chan []*sentry.Span, len(tests)) @@ -175,13 +202,14 @@ func TestIntegration(t *testing.T) { ctx = span.Context() request, err := http.NewRequestWithContext(ctx, tt.RequestMethod, tt.RequestURL, nil) - if err != nil { + if err != nil && !tt.WantError { t.Fatal(err) } roundTripper := &noopRoundTripper{ ExpectResponseStatus: tt.WantStatus, ExpectResponseLength: tt.WantResponseLength, + ExpectError: tt.WantError, } client := &http.Client{ @@ -189,11 +217,13 @@ func TestIntegration(t *testing.T) { } response, err := client.Do(request) - if err != nil { + if err != nil && !tt.WantError { t.Fatal(err) } - response.Body.Close() + if response != nil && response.Body != nil { + response.Body.Close() + } span.Finish() } @@ -234,6 +264,70 @@ func TestIntegration(t *testing.T) { } } +func TestIntegration_NoParentSpan(t *testing.T) { + spansCh := make(chan []*sentry.Span, 1) + + sentryClient, err := sentry.NewClient(sentry.ClientOptions{ + EnableTracing: true, + TracesSampleRate: 1.0, + BeforeSendTransaction: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { + spansCh <- event.Spans + return event + }, + }) + if err != nil { + t.Fatal(err) + } + + hub := sentry.NewHub(sentryClient, sentry.NewScope()) + ctx := sentry.SetHubOnContext(context.Background(), hub) + + request, err := http.NewRequestWithContext(ctx, "GET", "https://example.com", nil) + if err != nil { + t.Fatal(err) + } + + roundTripper := &noopRoundTripper{ + ExpectResponseStatus: 200, + ExpectResponseLength: 0, + } + + client := &http.Client{ + Transport: sentryhttpclient.NewSentryRoundTripper(roundTripper), + } + + response, err := client.Do(request) + if err != nil { + t.Fatal(err) + } + + response.Body.Close() + + if ok := sentryClient.Flush(testutils.FlushTimeout()); !ok { + t.Fatal("sentry.Flush timed out") + } + close(spansCh) + + var got [][]*sentry.Span + for e := range spansCh { + got = append(got, e) + } + + // Expect no spans. + if len(got) != 0 { + t.Errorf("Expected no spans, got %d", len(got)) + } + + // Expect "Baggage" and "Sentry-Trace" headers. + if value := response.Request.Header.Get("Baggage"); value != "" { + t.Errorf(`Expected "Baggage" header to be empty, got %s`, value) + } + + if value := response.Request.Header.Get("Sentry-Trace"); value == "" { + t.Errorf(`Expected "Sentry-Trace" header, got %s`, value) + } +} + func TestDefaults(t *testing.T) { t.Run("Create a regular outgoing HTTP request with default NewSentryRoundTripper", func(t *testing.T) { roundTripper := sentryhttpclient.NewSentryRoundTripper(nil) From 081fc0bb328cdbfff1c086bad1ae35b1e2963143 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 6 Nov 2024 20:58:32 +0700 Subject: [PATCH 10/17] feat(httpclient): trace propagation targets --- client.go | 4 + httpclient/sentryhttpclient.go | 62 +++++++++-- httpclient/sentryhttpclient_test.go | 163 ++++++++++++++++++++++++++-- 3 files changed, 208 insertions(+), 21 deletions(-) diff --git a/client.go b/client.go index b5b11b31d..38f02b197 100644 --- a/client.go +++ b/client.go @@ -133,6 +133,10 @@ type ClientOptions struct { TracesSampleRate float64 // Used to customize the sampling of traces, overrides TracesSampleRate. TracesSampler TracesSampler + // Control with URLs trace propagation should be enabled. Does not support regex patterns. + TracePropagationTargets []string + // When set to true, the SDK will start a span for outgoing HTTP OPTIONS requests. + TraceOptionsRequests bool // The sample rate for profiling traces in the range [0.0, 1.0]. // This is relative to TracesSampleRate - it is a ratio of profiled traces out of all sampled traces. ProfilesSampleRate float64 diff --git a/httpclient/sentryhttpclient.go b/httpclient/sentryhttpclient.go index 167740f99..e9d01ebc0 100644 --- a/httpclient/sentryhttpclient.go +++ b/httpclient/sentryhttpclient.go @@ -14,6 +14,7 @@ package sentryhttpclient import ( "fmt" "net/http" + "strings" "github.com/getsentry/sentry-go" ) @@ -21,19 +22,22 @@ import ( // SentryRoundTripTracerOption provides a specific type in which defines the option for SentryRoundTripper. type SentryRoundTripTracerOption func(*SentryRoundTripper) -// WithTags allows the RoundTripper to includes additional tags. -func WithTags(tags map[string]string) SentryRoundTripTracerOption { +// WithTracePropagationTargets configures additional trace propagation targets URL for the RoundTripper. +// Does not support regex patterns. +func WithTracePropagationTargets(targets []string) SentryRoundTripTracerOption { return func(t *SentryRoundTripper) { - for k, v := range tags { - t.tags[k] = v + if t.tracePropagationTargets == nil { + t.tracePropagationTargets = targets + } else { + t.tracePropagationTargets = append(t.tracePropagationTargets, targets...) } } } -// WithTag allows the RoundTripper to includes additional tag. -func WithTag(key, value string) SentryRoundTripTracerOption { +// WithTraceOptionsRequests overrides the default options for whether the tracer should trace OPTIONS requests. +func WithTraceOptionsRequests(traceOptionsRequests bool) SentryRoundTripTracerOption { return func(t *SentryRoundTripper) { - t.tags[key] = value + t.traceOptionsRequests = traceOptionsRequests } } @@ -45,9 +49,25 @@ func NewSentryRoundTripper(originalRoundTripper http.RoundTripper, opts ...Sentr originalRoundTripper = http.DefaultTransport } + // Configure trace propagation targets + var tracePropagationTargets []string + var traceOptionsRequests bool + if hub := sentry.CurrentHub(); hub != nil { + client := hub.Client() + if client != nil { + clientOptions := client.Options() + if clientOptions.TracePropagationTargets != nil { + tracePropagationTargets = clientOptions.TracePropagationTargets + } + + traceOptionsRequests = clientOptions.TraceOptionsRequests + } + } + t := &SentryRoundTripper{ - originalRoundTripper: originalRoundTripper, - tags: make(map[string]string), + originalRoundTripper: originalRoundTripper, + tracePropagationTargets: tracePropagationTargets, + traceOptionsRequests: traceOptionsRequests, } for _, opt := range opts { @@ -63,10 +83,31 @@ func NewSentryRoundTripper(originalRoundTripper http.RoundTripper, opts ...Sentr type SentryRoundTripper struct { originalRoundTripper http.RoundTripper - tags map[string]string + tracePropagationTargets []string + traceOptionsRequests bool } func (s *SentryRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) { + if request.Method == http.MethodOptions && !s.traceOptionsRequests { + return s.originalRoundTripper.RoundTrip(request) + } + + // Respect trace propagation targets + if len(s.tracePropagationTargets) > 0 { + requestURL := request.URL.String() + foundMatch := false + for _, target := range s.tracePropagationTargets { + if strings.Contains(requestURL, target) { + foundMatch = true + break + } + } + + if !foundMatch { + return s.originalRoundTripper.RoundTrip(request) + } + } + // Only create the `http.client` span only if there is a parent span. parentSpan := sentry.SpanFromContext(request.Context()) if parentSpan == nil { @@ -81,7 +122,6 @@ func (s *SentryRoundTripper) RoundTrip(request *http.Request) (*http.Response, e cleanRequestURL := request.URL.Redacted() span := parentSpan.StartChild("http.client", sentry.WithTransactionName(fmt.Sprintf("%s %s", request.Method, cleanRequestURL))) - span.Tags = s.tags defer span.Finish() span.SetData("http.query", request.URL.Query().Encode()) diff --git a/httpclient/sentryhttpclient_test.go b/httpclient/sentryhttpclient_test.go index 0a81fa1e9..72a56182a 100644 --- a/httpclient/sentryhttpclient_test.go +++ b/httpclient/sentryhttpclient_test.go @@ -79,7 +79,6 @@ func TestIntegration(t *testing.T) { }, Name: "GET https://example.com/foo", Op: "http.client", - Tags: map[string]string{}, Origin: "manual", Sampled: sentry.SampledTrue, Status: sentry.SpanStatusOK, @@ -103,7 +102,6 @@ func TestIntegration(t *testing.T) { }, Name: "GET https://example.com:443/foo/bar?baz=123#readme", Op: "http.client", - Tags: map[string]string{}, Origin: "manual", Sampled: sentry.SampledTrue, Status: sentry.SpanStatusOK, @@ -112,7 +110,7 @@ func TestIntegration(t *testing.T) { { RequestMethod: "HEAD", RequestURL: "https://example.com:8443/foo?bar=123&abc=def", - TracerOptions: []sentryhttpclient.SentryRoundTripTracerOption{sentryhttpclient.WithTag("user", "def"), sentryhttpclient.WithTags(map[string]string{"domain": "example.com"})}, + TracerOptions: []sentryhttpclient.SentryRoundTripTracerOption{}, WantStatus: 400, WantResponseLength: 0, WantSpan: &sentry.Span{ @@ -125,10 +123,7 @@ func TestIntegration(t *testing.T) { "server.address": string("example.com"), "server.port": string("8443"), }, - Tags: map[string]string{ - "user": "def", - "domain": "example.com", - }, + Name: "HEAD https://example.com:8443/foo?bar=123&abc=def", Op: "http.client", Origin: "manual", @@ -153,7 +148,6 @@ func TestIntegration(t *testing.T) { }, Name: "POST https://john:xxxxx@example.com:4321/secret", Op: "http.client", - Tags: map[string]string{}, Origin: "manual", Sampled: sentry.SampledTrue, Status: sentry.SpanStatusOK, @@ -173,12 +167,72 @@ func TestIntegration(t *testing.T) { }, Name: "POST https://example.com", Op: "http.client", - Tags: map[string]string{}, Origin: "manual", Sampled: sentry.SampledTrue, Status: sentry.SpanStatusInternalError, }, }, + { + RequestMethod: "OPTIONS", + RequestURL: "https://example.com", + WantError: false, + WantSpan: nil, + }, + { + RequestMethod: "OPTIONS", + RequestURL: "https://example.com", + TracerOptions: []sentryhttpclient.SentryRoundTripTracerOption{sentryhttpclient.WithTraceOptionsRequests(true)}, + WantStatus: 204, + WantResponseLength: 0, + WantSpan: &sentry.Span{ + Data: map[string]interface{}{ + "http.fragment": string(""), + "http.query": string(""), + "http.request.method": string("OPTIONS"), + "http.response.status_code": int(204), + "http.response_content_length": int64(0), + "server.address": string("example.com"), + "server.port": string(""), + }, + + Name: "OPTIONS https://example.com", + Op: "http.client", + Origin: "manual", + Sampled: sentry.SampledTrue, + Status: sentry.SpanStatusOK, + }, + }, + { + RequestMethod: "GET", + RequestURL: "https://example.com/foo/bar?baz=123#readme", + TracerOptions: []sentryhttpclient.SentryRoundTripTracerOption{sentryhttpclient.WithTracePropagationTargets([]string{"example.com"}), sentryhttpclient.WithTracePropagationTargets([]string{"example.org"})}, + WantStatus: 200, + WantResponseLength: 0, + WantSpan: &sentry.Span{ + Data: map[string]interface{}{ + "http.fragment": string("readme"), + "http.query": string("baz=123"), + "http.request.method": string("GET"), + "http.response.status_code": int(200), + "http.response_content_length": int64(0), + "server.address": string("example.com"), + "server.port": string(""), + }, + Name: "GET https://example.com/foo/bar?baz=123#readme", + Op: "http.client", + Origin: "manual", + Sampled: sentry.SampledTrue, + Status: sentry.SpanStatusOK, + }, + }, + { + RequestMethod: "GET", + RequestURL: "https://example.net/foo/bar?baz=123#readme", + TracerOptions: []sentryhttpclient.SentryRoundTripTracerOption{sentryhttpclient.WithTracePropagationTargets([]string{"example.com"})}, + WantStatus: 200, + WantResponseLength: 0, + WantSpan: nil, + }, } spansCh := make(chan []*sentry.Span, len(tests)) @@ -258,12 +312,101 @@ func TestIntegration(t *testing.T) { } } - if !foundMatch { + if tt.WantSpan != nil && !foundMatch { t.Errorf("Span mismatch (-want +got):\n%s", strings.Join(diffs, "\n")) + } else if tt.WantSpan == nil && foundMatch { + t.Errorf("Expected no span, got %+v", gotSpans) } } } +func TestIntegration_GlobalClientOptions(t *testing.T) { + spansCh := make(chan []*sentry.Span, 1) + + err := sentry.Init(sentry.ClientOptions{ + EnableTracing: true, + TracePropagationTargets: []string{"example.com"}, + TraceOptionsRequests: false, + TracesSampleRate: 1.0, + BeforeSendTransaction: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { + spansCh <- event.Spans + return event + }, + }) + if err != nil { + t.Fatal(err) + } + + ctx := sentry.SetHubOnContext(context.Background(), sentry.CurrentHub()) + span := sentry.StartSpan(ctx, "fake_parent", sentry.WithTransactionName("Fake Parent")) + ctx = span.Context() + + request, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://example.com", nil) + if err != nil { + t.Fatal(err) + } + + roundTripper := &noopRoundTripper{ + ExpectResponseStatus: 200, + ExpectResponseLength: 48, + ExpectError: false, + } + + client := &http.Client{ + Transport: sentryhttpclient.NewSentryRoundTripper(roundTripper), + } + + response, err := client.Do(request) + if err != nil { + t.Fatal(err) + } + + if response != nil && response.Body != nil { + response.Body.Close() + } + span.Finish() + + if ok := sentry.Flush(testutils.FlushTimeout()); !ok { + t.Fatal("sentry.Flush timed out") + } + close(spansCh) + + var got []*sentry.Span + for e := range spansCh { + got = append(got, e...) + } + + optstrans := cmp.Options{ + cmpopts.IgnoreFields( + sentry.Span{}, + "TraceID", "SpanID", "ParentSpanID", "StartTime", "EndTime", + "mu", "parent", "sampleRate", "ctx", "dynamicSamplingContext", "recorder", "finishOnce", "collectProfile", "contexts", + ), + } + + gotSpan := got[0] + wantSpan := &sentry.Span{ + Data: map[string]interface{}{ + "http.fragment": string(""), + "http.query": string(""), + "http.request.method": string("POST"), + "http.response.status_code": int(200), + "http.response_content_length": int64(48), + "server.address": string("example.com"), + "server.port": string(""), + }, + Name: "POST https://example.com", + Op: "http.client", + Origin: "manual", + Sampled: sentry.SampledTrue, + Status: sentry.SpanStatusOK, + } + + if diff := cmp.Diff(wantSpan, gotSpan, optstrans); diff != "" { + t.Errorf("Span mismatch (-want +got):\n%s", diff) + } +} + func TestIntegration_NoParentSpan(t *testing.T) { spansCh := make(chan []*sentry.Span, 1) From a3a6367bef5a7cc062ffc6d2ec00ddaa0d2cf831 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Sat, 16 Nov 2024 18:02:14 +0700 Subject: [PATCH 11/17] chore: remove trace options request --- client.go | 2 -- httpclient/sentryhttpclient.go | 15 --------------- httpclient/sentryhttpclient_test.go | 25 ------------------------- 3 files changed, 42 deletions(-) diff --git a/client.go b/client.go index 38f02b197..1015fa0a2 100644 --- a/client.go +++ b/client.go @@ -135,8 +135,6 @@ type ClientOptions struct { TracesSampler TracesSampler // Control with URLs trace propagation should be enabled. Does not support regex patterns. TracePropagationTargets []string - // When set to true, the SDK will start a span for outgoing HTTP OPTIONS requests. - TraceOptionsRequests bool // The sample rate for profiling traces in the range [0.0, 1.0]. // This is relative to TracesSampleRate - it is a ratio of profiled traces out of all sampled traces. ProfilesSampleRate float64 diff --git a/httpclient/sentryhttpclient.go b/httpclient/sentryhttpclient.go index e9d01ebc0..87cb2479f 100644 --- a/httpclient/sentryhttpclient.go +++ b/httpclient/sentryhttpclient.go @@ -34,13 +34,6 @@ func WithTracePropagationTargets(targets []string) SentryRoundTripTracerOption { } } -// WithTraceOptionsRequests overrides the default options for whether the tracer should trace OPTIONS requests. -func WithTraceOptionsRequests(traceOptionsRequests bool) SentryRoundTripTracerOption { - return func(t *SentryRoundTripper) { - t.traceOptionsRequests = traceOptionsRequests - } -} - // NewSentryRoundTripper provides a wrapper to existing http.RoundTripper to have required span data and trace headers for outgoing HTTP requests. // // - If `nil` is passed to `originalRoundTripper`, it will use http.DefaultTransport instead. @@ -51,7 +44,6 @@ func NewSentryRoundTripper(originalRoundTripper http.RoundTripper, opts ...Sentr // Configure trace propagation targets var tracePropagationTargets []string - var traceOptionsRequests bool if hub := sentry.CurrentHub(); hub != nil { client := hub.Client() if client != nil { @@ -60,14 +52,12 @@ func NewSentryRoundTripper(originalRoundTripper http.RoundTripper, opts ...Sentr tracePropagationTargets = clientOptions.TracePropagationTargets } - traceOptionsRequests = clientOptions.TraceOptionsRequests } } t := &SentryRoundTripper{ originalRoundTripper: originalRoundTripper, tracePropagationTargets: tracePropagationTargets, - traceOptionsRequests: traceOptionsRequests, } for _, opt := range opts { @@ -84,14 +74,9 @@ type SentryRoundTripper struct { originalRoundTripper http.RoundTripper tracePropagationTargets []string - traceOptionsRequests bool } func (s *SentryRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) { - if request.Method == http.MethodOptions && !s.traceOptionsRequests { - return s.originalRoundTripper.RoundTrip(request) - } - // Respect trace propagation targets if len(s.tracePropagationTargets) > 0 { requestURL := request.URL.String() diff --git a/httpclient/sentryhttpclient_test.go b/httpclient/sentryhttpclient_test.go index 72a56182a..8bca80b74 100644 --- a/httpclient/sentryhttpclient_test.go +++ b/httpclient/sentryhttpclient_test.go @@ -178,30 +178,6 @@ func TestIntegration(t *testing.T) { WantError: false, WantSpan: nil, }, - { - RequestMethod: "OPTIONS", - RequestURL: "https://example.com", - TracerOptions: []sentryhttpclient.SentryRoundTripTracerOption{sentryhttpclient.WithTraceOptionsRequests(true)}, - WantStatus: 204, - WantResponseLength: 0, - WantSpan: &sentry.Span{ - Data: map[string]interface{}{ - "http.fragment": string(""), - "http.query": string(""), - "http.request.method": string("OPTIONS"), - "http.response.status_code": int(204), - "http.response_content_length": int64(0), - "server.address": string("example.com"), - "server.port": string(""), - }, - - Name: "OPTIONS https://example.com", - Op: "http.client", - Origin: "manual", - Sampled: sentry.SampledTrue, - Status: sentry.SpanStatusOK, - }, - }, { RequestMethod: "GET", RequestURL: "https://example.com/foo/bar?baz=123#readme", @@ -326,7 +302,6 @@ func TestIntegration_GlobalClientOptions(t *testing.T) { err := sentry.Init(sentry.ClientOptions{ EnableTracing: true, TracePropagationTargets: []string{"example.com"}, - TraceOptionsRequests: false, TracesSampleRate: 1.0, BeforeSendTransaction: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { spansCh <- event.Spans From 1075de44e151d2c5e22355306f2d4ebe9c203306 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Thu, 24 Apr 2025 12:14:24 +0000 Subject: [PATCH 12/17] Update sentryhttpclient.go Co-authored-by: Giannis Gkiortzis <58184179+giortzisg@users.noreply.github.com> --- httpclient/sentryhttpclient.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpclient/sentryhttpclient.go b/httpclient/sentryhttpclient.go index 87cb2479f..4f3916204 100644 --- a/httpclient/sentryhttpclient.go +++ b/httpclient/sentryhttpclient.go @@ -135,6 +135,6 @@ func (s *SentryRoundTripper) RoundTrip(request *http.Request) (*http.Response, e // SentryHTTPClient provides a default HTTP client with SentryRoundTripper included. // This can be used directly to perform HTTP request. -var SentryHTTPClient = &http.Client{ +var Client = &http.Client{ Transport: NewSentryRoundTripper(http.DefaultTransport), } From ae861f3b9443ae4744a1ec4a75a57f39b195cd06 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Fri, 25 Apr 2025 13:23:09 +0700 Subject: [PATCH 13/17] feat(httpclient): use WithDescription instead of WithTransactionName --- httpclient/sentryhttpclient.go | 2 +- httpclient/sentryhttpclient_test.go | 79 +++++++++++++++-------------- 2 files changed, 41 insertions(+), 40 deletions(-) diff --git a/httpclient/sentryhttpclient.go b/httpclient/sentryhttpclient.go index 4f3916204..7ce6f5c57 100644 --- a/httpclient/sentryhttpclient.go +++ b/httpclient/sentryhttpclient.go @@ -106,7 +106,7 @@ func (s *SentryRoundTripper) RoundTrip(request *http.Request) (*http.Response, e cleanRequestURL := request.URL.Redacted() - span := parentSpan.StartChild("http.client", sentry.WithTransactionName(fmt.Sprintf("%s %s", request.Method, cleanRequestURL))) + span := parentSpan.StartChild("http.client", sentry.WithDescription(fmt.Sprintf("%s %s", request.Method, cleanRequestURL))) defer span.Finish() span.SetData("http.query", request.URL.Query().Encode()) diff --git a/httpclient/sentryhttpclient_test.go b/httpclient/sentryhttpclient_test.go index 8bca80b74..7d56d4631 100644 --- a/httpclient/sentryhttpclient_test.go +++ b/httpclient/sentryhttpclient_test.go @@ -77,11 +77,11 @@ func TestIntegration(t *testing.T) { "server.address": string("example.com"), "server.port": string(""), }, - Name: "GET https://example.com/foo", - Op: "http.client", - Origin: "manual", - Sampled: sentry.SampledTrue, - Status: sentry.SpanStatusOK, + Description: "GET https://example.com/foo", + Op: "http.client", + Origin: "manual", + Sampled: sentry.SampledTrue, + Status: sentry.SpanStatusOK, }, }, { @@ -100,11 +100,11 @@ func TestIntegration(t *testing.T) { "server.address": string("example.com"), "server.port": string("443"), }, - Name: "GET https://example.com:443/foo/bar?baz=123#readme", - Op: "http.client", - Origin: "manual", - Sampled: sentry.SampledTrue, - Status: sentry.SpanStatusOK, + Description: "GET https://example.com:443/foo/bar?baz=123#readme", + Op: "http.client", + Origin: "manual", + Sampled: sentry.SampledTrue, + Status: sentry.SpanStatusOK, }, }, { @@ -123,12 +123,11 @@ func TestIntegration(t *testing.T) { "server.address": string("example.com"), "server.port": string("8443"), }, - - Name: "HEAD https://example.com:8443/foo?bar=123&abc=def", - Op: "http.client", - Origin: "manual", - Sampled: sentry.SampledTrue, - Status: sentry.SpanStatusInvalidArgument, + Description: "HEAD https://example.com:8443/foo?bar=123&abc=def", + Op: "http.client", + Origin: "manual", + Sampled: sentry.SampledTrue, + Status: sentry.SpanStatusInvalidArgument, }, }, { @@ -146,11 +145,11 @@ func TestIntegration(t *testing.T) { "server.address": string("example.com"), "server.port": string("4321"), }, - Name: "POST https://john:xxxxx@example.com:4321/secret", - Op: "http.client", - Origin: "manual", - Sampled: sentry.SampledTrue, - Status: sentry.SpanStatusOK, + Description: "POST https://john:xxxxx@example.com:4321/secret", + Op: "http.client", + Origin: "manual", + Sampled: sentry.SampledTrue, + Status: sentry.SpanStatusOK, }, }, { @@ -165,11 +164,11 @@ func TestIntegration(t *testing.T) { "server.address": string("example.com"), "server.port": string(""), }, - Name: "POST https://example.com", - Op: "http.client", - Origin: "manual", - Sampled: sentry.SampledTrue, - Status: sentry.SpanStatusInternalError, + Description: "POST https://example.com", + Op: "http.client", + Origin: "manual", + Sampled: sentry.SampledTrue, + Status: sentry.SpanStatusInternalError, }, }, { @@ -194,11 +193,11 @@ func TestIntegration(t *testing.T) { "server.address": string("example.com"), "server.port": string(""), }, - Name: "GET https://example.com/foo/bar?baz=123#readme", - Op: "http.client", - Origin: "manual", - Sampled: sentry.SampledTrue, - Status: sentry.SpanStatusOK, + Description: "GET https://example.com/foo/bar?baz=123#readme", + Op: "http.client", + Origin: "manual", + Sampled: sentry.SampledTrue, + Status: sentry.SpanStatusOK, }, }, { @@ -271,7 +270,8 @@ func TestIntegration(t *testing.T) { cmpopts.IgnoreFields( sentry.Span{}, "TraceID", "SpanID", "ParentSpanID", "StartTime", "EndTime", - "mu", "parent", "sampleRate", "ctx", "dynamicSamplingContext", "recorder", "finishOnce", "collectProfile", "contexts", + "mu", "parent", "sampleRate", "ctx", "dynamicSamplingContext", "recorder", "finishOnce", "contexts", + "explicitSampled", ), } for i, tt := range tests { @@ -355,7 +355,8 @@ func TestIntegration_GlobalClientOptions(t *testing.T) { cmpopts.IgnoreFields( sentry.Span{}, "TraceID", "SpanID", "ParentSpanID", "StartTime", "EndTime", - "mu", "parent", "sampleRate", "ctx", "dynamicSamplingContext", "recorder", "finishOnce", "collectProfile", "contexts", + "mu", "parent", "sampleRate", "ctx", "dynamicSamplingContext", "recorder", "finishOnce", "contexts", + "explicitSampled", ), } @@ -370,11 +371,11 @@ func TestIntegration_GlobalClientOptions(t *testing.T) { "server.address": string("example.com"), "server.port": string(""), }, - Name: "POST https://example.com", - Op: "http.client", - Origin: "manual", - Sampled: sentry.SampledTrue, - Status: sentry.SpanStatusOK, + Description: "POST https://example.com", + Op: "http.client", + Origin: "manual", + Sampled: sentry.SampledTrue, + Status: sentry.SpanStatusOK, } if diff := cmp.Diff(wantSpan, gotSpan, optstrans); diff != "" { @@ -462,7 +463,7 @@ func TestDefaults(t *testing.T) { }) t.Run("Create a regular outgoing HTTP request with default SentryHttpClient", func(t *testing.T) { - client := sentryhttpclient.SentryHTTPClient + client := sentryhttpclient.Client res, err := client.Head("https://sentry.io") if err != nil { From a5eb96fd22e27a27d035c4e61c308d12094287ea Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Mon, 28 Apr 2025 15:28:16 +0700 Subject: [PATCH 14/17] Update _examples/httpclient/main.go Co-authored-by: Giannis Gkiortzis <58184179+giortzisg@users.noreply.github.com> --- _examples/httpclient/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_examples/httpclient/main.go b/_examples/httpclient/main.go index 0dfadb343..940762682 100644 --- a/_examples/httpclient/main.go +++ b/_examples/httpclient/main.go @@ -38,7 +38,7 @@ func main() { } // With Sentry's HTTP client - err = getExamplePage(ctx, sentryhttpclient.SentryHTTPClient) + err = getExamplePage(ctx, sentryhttpclient.Client) if err != nil { panic(err) } From 791feb523aff464aae8e3535ef235c5059c7fc36 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Fri, 2 May 2025 08:01:13 +0700 Subject: [PATCH 15/17] chore: lint errors --- httpclient/sentryhttpclient.go | 1 - httpclient/sentryhttpclient_test.go | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/httpclient/sentryhttpclient.go b/httpclient/sentryhttpclient.go index 7ce6f5c57..c27e4098a 100644 --- a/httpclient/sentryhttpclient.go +++ b/httpclient/sentryhttpclient.go @@ -51,7 +51,6 @@ func NewSentryRoundTripper(originalRoundTripper http.RoundTripper, opts ...Sentr if clientOptions.TracePropagationTargets != nil { tracePropagationTargets = clientOptions.TracePropagationTargets } - } } diff --git a/httpclient/sentryhttpclient_test.go b/httpclient/sentryhttpclient_test.go index 7d56d4631..269e19492 100644 --- a/httpclient/sentryhttpclient_test.go +++ b/httpclient/sentryhttpclient_test.go @@ -215,7 +215,7 @@ func TestIntegration(t *testing.T) { sentryClient, err := sentry.NewClient(sentry.ClientOptions{ EnableTracing: true, TracesSampleRate: 1.0, - BeforeSendTransaction: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { + BeforeSendTransaction: func(event *sentry.Event, _ *sentry.EventHint) *sentry.Event { spansCh <- event.Spans return event }, @@ -303,7 +303,7 @@ func TestIntegration_GlobalClientOptions(t *testing.T) { EnableTracing: true, TracePropagationTargets: []string{"example.com"}, TracesSampleRate: 1.0, - BeforeSendTransaction: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { + BeforeSendTransaction: func(event *sentry.Event, _s *sentry.EventHint) *sentry.Event { spansCh <- event.Spans return event }, @@ -389,7 +389,7 @@ func TestIntegration_NoParentSpan(t *testing.T) { sentryClient, err := sentry.NewClient(sentry.ClientOptions{ EnableTracing: true, TracesSampleRate: 1.0, - BeforeSendTransaction: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { + BeforeSendTransaction: func(event *sentry.Event, _ *sentry.EventHint) *sentry.Event { spansCh <- event.Spans return event }, From c912fe905c4a6edc5b75233905f591545f250006 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Fri, 2 May 2025 09:41:39 +0700 Subject: [PATCH 16/17] chore: omit variable name --- httpclient/sentryhttpclient_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpclient/sentryhttpclient_test.go b/httpclient/sentryhttpclient_test.go index 269e19492..4ec1e50f9 100644 --- a/httpclient/sentryhttpclient_test.go +++ b/httpclient/sentryhttpclient_test.go @@ -303,7 +303,7 @@ func TestIntegration_GlobalClientOptions(t *testing.T) { EnableTracing: true, TracePropagationTargets: []string{"example.com"}, TracesSampleRate: 1.0, - BeforeSendTransaction: func(event *sentry.Event, _s *sentry.EventHint) *sentry.Event { + BeforeSendTransaction: func(event *sentry.Event, _ *sentry.EventHint) *sentry.Event { spansCh <- event.Spans return event }, From 912797fa4aaa7ce770d5775d8064c765166a9a1e Mon Sep 17 00:00:00 2001 From: Michi Hoffmann Date: Tue, 6 May 2025 11:18:31 +0200 Subject: [PATCH 17/17] Update CHANGELOG.md --- CHANGELOG.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bbc9a555..41e926929 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,5 @@ # Changelog -## Unreleased - -### Features - -- Add net/http client integration ([#876](https://github.com/getsentry/sentry-go/pull/876)) - ## 0.32.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.32.0.