Skip to content

Commit

Permalink
feat: Add support for Dynamic Sampling (#491)
Browse files Browse the repository at this point in the history
Co-authored-by: Kamil Ogórek <kamil@sentry.io>
Co-authored-by: Abhijeet Prasad <devabhiprasad@gmail.com>
  • Loading branch information
3 people committed Dec 5, 2022
1 parent 5dbb801 commit c63acf9
Show file tree
Hide file tree
Showing 19 changed files with 1,122 additions and 78 deletions.
3 changes: 1 addition & 2 deletions .craft.yml
@@ -1,6 +1,5 @@
minVersion: 0.23.1
preReleaseCommand: bash scripts/craft-pre-release.sh
changelogPolicy: auto
changelogPolicy: simple
artifactProvider:
name: none
targets:
Expand Down
6 changes: 3 additions & 3 deletions client.go
Expand Up @@ -621,12 +621,12 @@ func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventMod

event.Platform = "go"
event.Sdk = SdkInfo{
Name: "sentry.go",
Version: Version,
Name: SDKIdentifier,
Version: SDKVersion,
Integrations: client.listIntegrations(),
Packages: []SdkPackage{{
Name: "sentry-go",
Version: Version,
Version: SDKVersion,
}},
}

Expand Down
51 changes: 31 additions & 20 deletions client_test.go
Expand Up @@ -78,11 +78,15 @@ func TestCaptureMessageEmptyString(t *testing.T) {
},
}
got := transport.lastEvent
opts := cmp.Transformer("SimplifiedEvent", func(e *Event) *Event {
return &Event{
Exception: e.Exception,
}
})
opts := cmp.Options{
cmpopts.IgnoreFields(Event{}, "sdkMetaData"),
cmp.Transformer("SimplifiedEvent", func(e *Event) *Event {
return &Event{
Exception: e.Exception,
}
}),
}

if diff := cmp.Diff(want, got, opts); diff != "" {
t.Errorf("(-want +got):\n%s", diff)
}
Expand Down Expand Up @@ -266,14 +270,14 @@ func TestCaptureEvent(t *testing.T) {
Platform: "go",
Sdk: SdkInfo{
Name: "sentry.go",
Version: Version,
Version: SDKVersion,
Integrations: []string{},
Packages: []SdkPackage{
{
// FIXME: name format doesn't follow spec in
// https://docs.sentry.io/development/sdk-dev/event-payloads/sdk/
Name: "sentry-go",
Version: Version,
Version: SDKVersion,
},
// TODO: perhaps the list of packages is incomplete or there
// should not be any package at all. We may include references
Expand All @@ -282,7 +286,7 @@ func TestCaptureEvent(t *testing.T) {
},
}
got := transport.lastEvent
opts := cmp.Options{cmpopts.IgnoreFields(Event{}, "Release")}
opts := cmp.Options{cmpopts.IgnoreFields(Event{}, "Release", "sdkMetaData")}
if diff := cmp.Diff(want, got, opts); diff != "" {
t.Errorf("Event mismatch (-want +got):\n%s", diff)
}
Expand All @@ -309,11 +313,14 @@ func TestCaptureEventNil(t *testing.T) {
},
}
got := transport.lastEvent
opts := cmp.Transformer("SimplifiedEvent", func(e *Event) *Event {
return &Event{
Exception: e.Exception,
}
})
opts := cmp.Options{
cmpopts.IgnoreFields(Event{}, "sdkMetaData"),
cmp.Transformer("SimplifiedEvent", func(e *Event) *Event {
return &Event{
Exception: e.Exception,
}
}),
}
if diff := cmp.Diff(want, got, opts); diff != "" {
t.Errorf("(-want +got):\n%s", diff)
}
Expand Down Expand Up @@ -476,13 +483,17 @@ func TestRecover(t *testing.T) {
t.Fatalf("events = %s\ngot %d events, want 1", b, len(events))
}
got := events[0]
opts := cmp.Transformer("SimplifiedEvent", func(e *Event) *Event {
return &Event{
Message: e.Message,
Exception: e.Exception,
Level: e.Level,
}
})
opts := cmp.Options{
cmpopts.IgnoreFields(Event{}, "sdkMetaData"),
cmp.Transformer("SimplifiedEvent", func(e *Event) *Event {
return &Event{
Message: e.Message,
Exception: e.Exception,
Level: e.Level,
}
}),
}

if diff := cmp.Diff(want, got, opts); diff != "" {
t.Errorf("(-want +got):\n%s", diff)
}
Expand Down
110 changes: 110 additions & 0 deletions dynamic_sampling_context.go
@@ -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 ""
}
150 changes: 150 additions & 0 deletions dynamic_sampling_context_test.go
@@ -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)
}
1 change: 1 addition & 0 deletions fasthttp/sentryfasthttp_test.go
Expand Up @@ -208,6 +208,7 @@ func TestIntegration(t *testing.T) {
sentry.Event{},
"Contexts", "EventID", "Extra", "Platform", "Modules",
"Release", "Sdk", "ServerName", "Tags", "Timestamp",
"sdkMetaData",
),
cmpopts.IgnoreMapEntries(func(k string, v string) bool {
// fasthttp changed Content-Length behavior in
Expand Down

0 comments on commit c63acf9

Please sign in to comment.