-
Notifications
You must be signed in to change notification settings - Fork 215
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add support for Dynamic Sampling (#491)
Co-authored-by: Kamil Ogórek <kamil@sentry.io> Co-authored-by: Abhijeet Prasad <devabhiprasad@gmail.com>
- Loading branch information
1 parent
5dbb801
commit c63acf9
Showing
19 changed files
with
1,122 additions
and
78 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
package sentry | ||
|
||
import ( | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/getsentry/sentry-go/internal/otel/baggage" | ||
) | ||
|
||
const ( | ||
sentryPrefix = "sentry-" | ||
) | ||
|
||
// DynamicSamplingContext holds information about the current event that can be used to make dynamic sampling decisions. | ||
type DynamicSamplingContext struct { | ||
Entries map[string]string | ||
Frozen bool | ||
} | ||
|
||
func DynamicSamplingContextFromHeader(header []byte) (DynamicSamplingContext, error) { | ||
bag, err := baggage.Parse(string(header)) | ||
if err != nil { | ||
return DynamicSamplingContext{}, err | ||
} | ||
|
||
entries := map[string]string{} | ||
for _, member := range bag.Members() { | ||
// We only store baggage members if their key starts with "sentry-". | ||
if k, v := member.Key(), member.Value(); strings.HasPrefix(k, sentryPrefix) { | ||
entries[strings.TrimPrefix(k, sentryPrefix)] = v | ||
} | ||
} | ||
|
||
return DynamicSamplingContext{ | ||
Entries: entries, | ||
Frozen: true, | ||
}, nil | ||
} | ||
|
||
func DynamicSamplingContextFromTransaction(span *Span) DynamicSamplingContext { | ||
entries := map[string]string{} | ||
|
||
hub := hubFromContext(span.Context()) | ||
scope := hub.Scope() | ||
client := hub.Client() | ||
options := client.Options() | ||
|
||
if traceID := span.TraceID.String(); traceID != "" { | ||
entries["trace_id"] = traceID | ||
} | ||
if sampleRate := span.sampleRate; sampleRate != 0 { | ||
entries["sample_rate"] = strconv.FormatFloat(sampleRate, 'f', -1, 64) | ||
} | ||
|
||
if dsn := client.dsn; dsn != nil { | ||
if publicKey := dsn.publicKey; publicKey != "" { | ||
entries["public_key"] = publicKey | ||
} | ||
} | ||
if release := options.Release; release != "" { | ||
entries["release"] = release | ||
} | ||
if environment := options.Environment; environment != "" { | ||
entries["environment"] = environment | ||
} | ||
|
||
// Only include the transaction name if it's of good quality (not empty and not SourceURL) | ||
if span.Source != "" && span.Source != SourceURL { | ||
if transactionName := scope.Transaction(); transactionName != "" { | ||
entries["transaction"] = transactionName | ||
} | ||
} | ||
|
||
if userSegment := scope.user.Segment; userSegment != "" { | ||
entries["user_segment"] = userSegment | ||
} | ||
|
||
return DynamicSamplingContext{ | ||
Entries: entries, | ||
Frozen: true, | ||
} | ||
} | ||
|
||
func (d DynamicSamplingContext) HasEntries() bool { | ||
return len(d.Entries) > 0 | ||
} | ||
|
||
func (d DynamicSamplingContext) IsFrozen() bool { | ||
return d.Frozen | ||
} | ||
|
||
func (d DynamicSamplingContext) String() string { | ||
members := []baggage.Member{} | ||
for k, entry := range d.Entries { | ||
member, err := baggage.NewMember(sentryPrefix+k, entry) | ||
if err != nil { | ||
continue | ||
} | ||
members = append(members, member) | ||
} | ||
if len(members) > 0 { | ||
baggage, err := baggage.New(members...) | ||
if err != nil { | ||
return "" | ||
} | ||
return baggage.String() | ||
} | ||
|
||
return "" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
package sentry | ||
|
||
import ( | ||
"strings" | ||
"testing" | ||
) | ||
|
||
func TestDynamicSamplingContextFromHeader(t *testing.T) { | ||
tests := []struct { | ||
input []byte | ||
want DynamicSamplingContext | ||
}{ | ||
{ | ||
input: []byte(""), | ||
want: DynamicSamplingContext{ | ||
Frozen: true, | ||
Entries: map[string]string{}, | ||
}, | ||
}, | ||
{ | ||
input: []byte("sentry-trace_id=d49d9bf66f13450b81f65bc51cf49c03,sentry-public_key=public,sentry-sample_rate=1"), | ||
want: DynamicSamplingContext{ | ||
Frozen: true, | ||
Entries: map[string]string{ | ||
"trace_id": "d49d9bf66f13450b81f65bc51cf49c03", | ||
"public_key": "public", | ||
"sample_rate": "1", | ||
}, | ||
}, | ||
}, | ||
{ | ||
input: []byte("sentry-trace_id=d49d9bf66f13450b81f65bc51cf49c03,sentry-public_key=public,sentry-sample_rate=1,foo=bar;foo;bar;bar=baz"), | ||
want: DynamicSamplingContext{ | ||
Frozen: true, | ||
Entries: map[string]string{ | ||
"trace_id": "d49d9bf66f13450b81f65bc51cf49c03", | ||
"public_key": "public", | ||
"sample_rate": "1", | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
for _, tc := range tests { | ||
got, err := DynamicSamplingContextFromHeader(tc.input) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
assertEqual(t, got, tc.want) | ||
} | ||
} | ||
|
||
func TestDynamicSamplingContextFromTransaction(t *testing.T) { | ||
tests := []struct { | ||
input *Span | ||
want DynamicSamplingContext | ||
}{ | ||
// Normal flow | ||
{ | ||
input: func() *Span { | ||
ctx := NewTestContext(ClientOptions{ | ||
EnableTracing: true, | ||
TracesSampleRate: 0.5, | ||
Dsn: "http://public@example.com/sentry/1", | ||
Release: "1.0.0", | ||
Environment: "test", | ||
}) | ||
hubFromContext(ctx).ConfigureScope(func(scope *Scope) { | ||
scope.SetUser(User{Segment: "user_segment"}) | ||
}) | ||
txn := StartTransaction(ctx, "name", TransctionSource(SourceCustom)) | ||
txn.TraceID = TraceIDFromHex("d49d9bf66f13450b81f65bc51cf49c03") | ||
return txn | ||
}(), | ||
want: DynamicSamplingContext{ | ||
Frozen: true, | ||
Entries: map[string]string{ | ||
"sample_rate": "0.5", | ||
"trace_id": "d49d9bf66f13450b81f65bc51cf49c03", | ||
"public_key": "public", | ||
"release": "1.0.0", | ||
"environment": "test", | ||
"transaction": "name", | ||
"user_segment": "user_segment", | ||
}, | ||
}, | ||
}, | ||
// Transaction with source url, do not include in Dynamic Sampling context | ||
{ | ||
input: func() *Span { | ||
ctx := NewTestContext(ClientOptions{ | ||
EnableTracing: true, | ||
TracesSampleRate: 0.5, | ||
Dsn: "http://public@example.com/sentry/1", | ||
Release: "1.0.0", | ||
}) | ||
txn := StartTransaction(ctx, "name", TransctionSource(SourceURL)) | ||
txn.TraceID = TraceIDFromHex("d49d9bf66f13450b81f65bc51cf49c03") | ||
return txn | ||
}(), | ||
want: DynamicSamplingContext{ | ||
Frozen: true, | ||
Entries: map[string]string{ | ||
"sample_rate": "0.5", | ||
"trace_id": "d49d9bf66f13450b81f65bc51cf49c03", | ||
"public_key": "public", | ||
"release": "1.0.0", | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
for _, tc := range tests { | ||
got := DynamicSamplingContextFromTransaction(tc.input) | ||
assertEqual(t, got, tc.want) | ||
} | ||
} | ||
|
||
func TestHasEntries(t *testing.T) { | ||
var dsc DynamicSamplingContext | ||
|
||
dsc = DynamicSamplingContext{} | ||
assertEqual(t, dsc.HasEntries(), false) | ||
|
||
dsc = DynamicSamplingContext{ | ||
Entries: map[string]string{ | ||
"foo": "bar", | ||
}, | ||
} | ||
assertEqual(t, dsc.HasEntries(), true) | ||
} | ||
|
||
func TestString(t *testing.T) { | ||
var dsc DynamicSamplingContext | ||
|
||
dsc = DynamicSamplingContext{} | ||
assertEqual(t, dsc.String(), "") | ||
|
||
dsc = DynamicSamplingContext{ | ||
Frozen: true, | ||
Entries: map[string]string{ | ||
"trace_id": "d49d9bf66f13450b81f65bc51cf49c03", | ||
"public_key": "public", | ||
"sample_rate": "1", | ||
}, | ||
} | ||
assertEqual(t, strings.Contains(dsc.String(), "sentry-trace_id=d49d9bf66f13450b81f65bc51cf49c03"), true) | ||
assertEqual(t, strings.Contains(dsc.String(), "sentry-public_key=public"), true) | ||
assertEqual(t, strings.Contains(dsc.String(), "sentry-sample_rate=1"), true) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.