Skip to content

Commit 8921d3c

Browse files
authored
feat: add TraceIgnoreStatusCodes option (#1089)
1 parent b07461a commit 8921d3c

File tree

4 files changed

+290
-0
lines changed

4 files changed

+290
-0
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"net/http"
7+
"time"
8+
9+
"github.com/getsentry/sentry-go"
10+
sentryhttp "github.com/getsentry/sentry-go/http"
11+
)
12+
13+
func main() {
14+
// Initialize Sentry with TraceIgnoreStatusCodes configuration
15+
err := sentry.Init(sentry.ClientOptions{
16+
Dsn: "", // Replace with your DSN
17+
Debug: true,
18+
EnableTracing: true,
19+
TracesSampleRate: 1.0,
20+
// Configure which HTTP status codes should not be traced
21+
// Each element can be a single code {code} or a range {min, max}
22+
TraceIgnoreStatusCodes: [][]int{{404}, {500, 599}}, // Ignore 404 and server errors 500-599
23+
})
24+
if err != nil {
25+
log.Fatalf("sentry.Init: %s", err)
26+
}
27+
28+
defer sentry.Flush(2 * time.Second)
29+
30+
// Create a Sentry-instrumented HTTP handler
31+
sentryHandler := sentryhttp.New(sentryhttp.Options{})
32+
33+
http.HandleFunc("/", sentryHandler.HandleFunc(homeHandler))
34+
http.HandleFunc("/users/", sentryHandler.HandleFunc(usersHandler))
35+
http.HandleFunc("/forbidden", sentryHandler.HandleFunc(forbiddenHandler))
36+
http.HandleFunc("/error", sentryHandler.HandleFunc(errorHandler))
37+
38+
fmt.Println("Server starting on :8080")
39+
fmt.Println("Try these endpoints:")
40+
fmt.Println(" GET / - Returns 200 OK (will be traced)")
41+
fmt.Println(" GET /users/123 - Returns 200 OK (will be traced)")
42+
fmt.Println(" GET /nonexistent - Returns 404 Not Found (will NOT be traced - matches {404})")
43+
fmt.Println(" GET /forbidden - Returns 403 Forbidden (will be traced)")
44+
fmt.Println(" GET /error - Returns 500 Internal Server Error (will NOT be traced - in range {500, 599})")
45+
46+
log.Fatal(http.ListenAndServe(":8080", nil))
47+
}
48+
49+
func homeHandler(w http.ResponseWriter, r *http.Request) {
50+
if r.URL.Path != "/" {
51+
// This will return 404 and won't be traced due to our configuration (matches {404})
52+
http.NotFound(w, r)
53+
return
54+
}
55+
56+
if span := sentry.SpanFromContext(r.Context()); span != nil {
57+
span.SetTag("endpoint", "home")
58+
span.SetData("custom_data", "This is the home page")
59+
}
60+
61+
w.WriteHeader(http.StatusOK)
62+
fmt.Fprintf(w, "Welcome to the home page! This 200 response will be traced.\n")
63+
}
64+
65+
func usersHandler(w http.ResponseWriter, r *http.Request) {
66+
if span := sentry.SpanFromContext(r.Context()); span != nil {
67+
span.SetTag("endpoint", "users")
68+
span.SetData("user_id", r.URL.Path[7:])
69+
}
70+
71+
w.WriteHeader(http.StatusOK)
72+
fmt.Fprintf(w, "User profile page. This 200 response will be traced.\n")
73+
}
74+
75+
func forbiddenHandler(w http.ResponseWriter, r *http.Request) {
76+
if span := sentry.SpanFromContext(r.Context()); span != nil {
77+
span.SetTag("endpoint", "forbidden")
78+
span.SetData("reason", "Access denied")
79+
}
80+
81+
w.WriteHeader(http.StatusForbidden)
82+
fmt.Fprintf(w, "Access forbidden. This 403 response will be traced.\n")
83+
}
84+
85+
func errorHandler(w http.ResponseWriter, r *http.Request) {
86+
if span := sentry.SpanFromContext(r.Context()); span != nil {
87+
span.SetTag("endpoint", "error")
88+
span.SetData("error_type", "simulated_server_error")
89+
}
90+
91+
w.WriteHeader(http.StatusInternalServerError)
92+
fmt.Fprintf(w, "Internal server error. This 500 response will NOT be traced (in range 500-599).\n")
93+
}

client.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,21 @@ type ClientOptions struct {
228228
Tags map[string]string
229229
// EnableLogs controls when logs should be emitted.
230230
EnableLogs bool
231+
// TraceIgnoreStatusCodes is a list of HTTP status codes that should not be traced.
232+
// Each element can be either:
233+
// - A single-element slice [code] for a specific status code
234+
// - A two-element slice [min, max] for a range of status codes (inclusive)
235+
// When an HTTP request results in a status code that matches any of these codes or ranges,
236+
// the transaction will not be sent to Sentry.
237+
//
238+
// Examples:
239+
// [][]int{{404}} // ignore only status code 404
240+
// [][]int{{400, 405}} // ignore status codes 400-405
241+
// [][]int{{404}, {500}} // ignore status codes 404 and 500
242+
// [][]int{{404}, {400, 405}, {500, 599}} // ignore 404, range 400-405, and range 500-599
243+
//
244+
// By default, this is empty and all status codes are traced.
245+
TraceIgnoreStatusCodes [][]int
231246
}
232247

233248
// Client is the underlying processor that is used by the main API and Hub

client_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,132 @@ func TestIgnoreTransactions(t *testing.T) {
699699
}
700700
}
701701

702+
func TestTraceIgnoreStatusCode_EmptyCode(t *testing.T) {
703+
transport := &MockTransport{}
704+
ctx := NewTestContext(ClientOptions{
705+
EnableTracing: true,
706+
TracesSampleRate: 1.0,
707+
Transport: transport,
708+
})
709+
710+
transaction := StartTransaction(ctx, "test")
711+
// Transaction has no http.response.status_code
712+
transaction.Finish()
713+
714+
dropped := transport.lastEvent == nil
715+
assertEqual(t, dropped, false, "expected transaction to not be dropped")
716+
}
717+
718+
func TestTraceIgnoreStatusCodes(t *testing.T) {
719+
tests := map[string]struct {
720+
ignoreStatusCodes [][]int
721+
statusCode interface{}
722+
expectDrop bool
723+
}{
724+
"No ignored codes": {
725+
statusCode: 404,
726+
ignoreStatusCodes: [][]int{},
727+
expectDrop: false,
728+
},
729+
"Status code not in ignore ranges": {
730+
statusCode: 500,
731+
ignoreStatusCodes: [][]int{{400, 405}},
732+
expectDrop: false,
733+
},
734+
"404 in ignore range": {
735+
statusCode: 404,
736+
ignoreStatusCodes: [][]int{{400, 405}},
737+
expectDrop: true,
738+
},
739+
"403 in ignore range": {
740+
statusCode: 403,
741+
ignoreStatusCodes: [][]int{{400, 405}},
742+
expectDrop: true,
743+
},
744+
"200 not ignored": {
745+
statusCode: 200,
746+
ignoreStatusCodes: [][]int{{400, 405}},
747+
expectDrop: false,
748+
},
749+
"wrong code not ignored": {
750+
statusCode: "something",
751+
ignoreStatusCodes: [][]int{{400, 405}},
752+
expectDrop: false,
753+
},
754+
"Single status code as single-element slice": {
755+
statusCode: 404,
756+
ignoreStatusCodes: [][]int{{404}},
757+
expectDrop: true,
758+
},
759+
"Single status code not in single-element slice": {
760+
statusCode: 500,
761+
ignoreStatusCodes: [][]int{{404}},
762+
expectDrop: false,
763+
},
764+
"Multiple single codes": {
765+
statusCode: 500,
766+
ignoreStatusCodes: [][]int{{404}, {500}},
767+
expectDrop: true,
768+
},
769+
"Multiple ranges - code in first range": {
770+
statusCode: 404,
771+
ignoreStatusCodes: [][]int{{400, 405}, {500, 599}},
772+
expectDrop: true,
773+
},
774+
"Multiple ranges - code in second range": {
775+
statusCode: 500,
776+
ignoreStatusCodes: [][]int{{400, 405}, {500, 599}},
777+
expectDrop: true,
778+
},
779+
"Multiple ranges - code not in any range": {
780+
statusCode: 200,
781+
ignoreStatusCodes: [][]int{{400, 405}, {500, 599}},
782+
expectDrop: false,
783+
},
784+
"Mixed single codes and ranges": {
785+
statusCode: 404,
786+
ignoreStatusCodes: [][]int{{404}, {500, 599}},
787+
expectDrop: true,
788+
},
789+
"Mixed single codes and ranges - code in range": {
790+
statusCode: 500,
791+
ignoreStatusCodes: [][]int{{404}, {500, 599}},
792+
expectDrop: true,
793+
},
794+
"Mixed single codes and ranges - code not matched": {
795+
statusCode: 200,
796+
ignoreStatusCodes: [][]int{{404}, {500, 599}},
797+
expectDrop: false,
798+
},
799+
}
800+
801+
for name, tt := range tests {
802+
t.Run(name, func(t *testing.T) {
803+
transport := &MockTransport{}
804+
ctx := NewTestContext(ClientOptions{
805+
EnableTracing: true,
806+
TracesSampleRate: 1.0,
807+
Transport: transport,
808+
TraceIgnoreStatusCodes: tt.ignoreStatusCodes,
809+
})
810+
811+
transaction := StartTransaction(ctx, "test")
812+
// Simulate HTTP response data like the integrations do
813+
transaction.SetData("http.response.status_code", tt.statusCode)
814+
transaction.Finish()
815+
816+
dropped := transport.lastEvent == nil
817+
if tt.expectDrop != dropped {
818+
if tt.expectDrop {
819+
t.Errorf("expected transaction with status code %d to be dropped", tt.statusCode)
820+
} else {
821+
t.Errorf("expected transaction with status code %d not to be dropped", tt.statusCode)
822+
}
823+
}
824+
})
825+
}
826+
}
827+
702828
func TestSampleRate(t *testing.T) {
703829
tests := []struct {
704830
SampleRate float64

tracing.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,58 @@ func (s *Span) SetDynamicSamplingContext(dsc DynamicSamplingContext) {
347347
}
348348
}
349349

350+
// shouldIgnoreStatusCode checks if the transaction should be ignored based on HTTP status code.
351+
func (s *Span) shouldIgnoreStatusCode() bool {
352+
if !s.IsTransaction() {
353+
return false
354+
}
355+
356+
ignoreStatusCodes := s.clientOptions().TraceIgnoreStatusCodes
357+
if len(ignoreStatusCodes) == 0 {
358+
return false
359+
}
360+
361+
s.mu.Lock()
362+
statusCodeData, exists := s.Data["http.response.status_code"]
363+
s.mu.Unlock()
364+
365+
if !exists {
366+
return false
367+
}
368+
369+
statusCode, ok := statusCodeData.(int)
370+
if !ok {
371+
return false
372+
}
373+
374+
for _, ignoredRange := range ignoreStatusCodes {
375+
switch len(ignoredRange) {
376+
case 1:
377+
// Single status code
378+
if statusCode == ignoredRange[0] {
379+
s.mu.Lock()
380+
s.Sampled = SampledFalse
381+
s.mu.Unlock()
382+
DebugLogger.Printf("dropping transaction with status code %v: found in TraceIgnoreStatusCodes", statusCode)
383+
return true
384+
}
385+
case 2:
386+
// Range of status codes [min, max]
387+
if ignoredRange[0] <= statusCode && statusCode <= ignoredRange[1] {
388+
s.mu.Lock()
389+
s.Sampled = SampledFalse
390+
s.mu.Unlock()
391+
DebugLogger.Printf("dropping transaction with status code %v: found in TraceIgnoreStatusCodes range [%d, %d]", statusCode, ignoredRange[0], ignoredRange[1])
392+
return true
393+
}
394+
default:
395+
DebugLogger.Printf("incorrect TraceIgnoreStatusCodes format: %v", ignoredRange)
396+
}
397+
}
398+
399+
return false
400+
}
401+
350402
// doFinish runs the actual Span.Finish() logic.
351403
func (s *Span) doFinish() {
352404
if s.EndTime.IsZero() {
@@ -360,6 +412,10 @@ func (s *Span) doFinish() {
360412
}
361413
}
362414

415+
if s.shouldIgnoreStatusCode() {
416+
return
417+
}
418+
363419
if !s.Sampled.Bool() {
364420
return
365421
}

0 commit comments

Comments
 (0)