From 45db8e5fb794274ef9fa085099dce694f2215419 Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Thu, 3 Nov 2022 12:17:09 +0100 Subject: [PATCH 01/16] feat: Add support for Dynamic Sampling --- dynamic_sampling_context.go | 45 ++ dynamic_sampling_context_test.go | 50 ++ interfaces.go | 16 + internal/otel/baggage/baggage.go | 571 ++++++++++++++++++ .../otel/baggage/internal/baggage/baggage.go | 45 ++ tracing.go | 42 +- transport.go | 30 +- transport_test.go | 4 +- 8 files changed, 793 insertions(+), 10 deletions(-) create mode 100644 dynamic_sampling_context.go create mode 100644 dynamic_sampling_context_test.go create mode 100644 internal/otel/baggage/baggage.go create mode 100644 internal/otel/baggage/internal/baggage/baggage.go diff --git a/dynamic_sampling_context.go b/dynamic_sampling_context.go new file mode 100644 index 00000000..95a5307c --- /dev/null +++ b/dynamic_sampling_context.go @@ -0,0 +1,45 @@ +package sentry + +import ( + "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 NewDynamicSamplingContext(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 (d DynamicSamplingContext) HasEntries() bool { + return len(d.Entries) > 0 +} + +func (d DynamicSamplingContext) String() string { + return "" +} diff --git a/dynamic_sampling_context_test.go b/dynamic_sampling_context_test.go new file mode 100644 index 00000000..0be16d55 --- /dev/null +++ b/dynamic_sampling_context_test.go @@ -0,0 +1,50 @@ +package sentry + +import ( + "testing" +) + +func TestNewDynamicSamplingContext(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 := NewDynamicSamplingContext(tc.input) + if err != nil { + t.Fatal(err) + } + assertEqual(t, got, tc.want) + } +} diff --git a/interfaces.go b/interfaces.go index 6f792220..c082ab38 100644 --- a/interfaces.go +++ b/interfaces.go @@ -215,6 +215,18 @@ type Exception struct { Stacktrace *Stacktrace `json:"stacktrace,omitempty"` } +type SDKMetaDataKey = string + +const ( + DynamicSamplingContextKey SDKMetaDataKey = "DynamicSamplingContext" +) + +// SDKMetaData is a struct to stash data which is needed at some point in the SDK's event processing pipeline +// but which shouldn't get send to Sentry. +type SDKMetaData = struct { + DynamicSamplingContextKey DynamicSamplingContext +} + // EventID is a hexadecimal string representing a unique uuid4 for an Event. // An EventID must be 32 characters long, lowercase and not have any dashes. type EventID string @@ -251,6 +263,10 @@ type Event struct { Type string `json:"type,omitempty"` StartTime time.Time `json:"start_timestamp"` Spans []*Span `json:"spans,omitempty"` + + // The fields below are not part of the final JSON payload. + + SDKMetaData SDKMetaData `json:"-"` } // TODO: Event.Contexts map[string]interface{} => map[string]EventContext, diff --git a/internal/otel/baggage/baggage.go b/internal/otel/baggage/baggage.go new file mode 100644 index 00000000..384a916a --- /dev/null +++ b/internal/otel/baggage/baggage.go @@ -0,0 +1,571 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// From https://github.com/open-telemetry/opentelemetry-go/blob/main/baggage/baggage.go +package baggage + +import ( + "errors" + "fmt" + "net/url" + "regexp" + "strings" + + "github.com/getsentry/sentry-go/internal/otel/baggage/internal/baggage" +) + +const ( + maxMembers = 180 + maxBytesPerMembers = 4096 + maxBytesPerBaggageString = 8192 + + listDelimiter = "," + keyValueDelimiter = "=" + propertyDelimiter = ";" + + keyDef = `([\x21\x23-\x27\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5a\x5e-\x7a\x7c\x7e]+)` + valueDef = `([\x21\x23-\x2b\x2d-\x3a\x3c-\x5B\x5D-\x7e]*)` + keyValueDef = `\s*` + keyDef + `\s*` + keyValueDelimiter + `\s*` + valueDef + `\s*` +) + +var ( + keyRe = regexp.MustCompile(`^` + keyDef + `$`) + valueRe = regexp.MustCompile(`^` + valueDef + `$`) + propertyRe = regexp.MustCompile(`^(?:\s*` + keyDef + `\s*|` + keyValueDef + `)$`) +) + +var ( + errInvalidKey = errors.New("invalid key") + errInvalidValue = errors.New("invalid value") + errInvalidProperty = errors.New("invalid baggage list-member property") + errInvalidMember = errors.New("invalid baggage list-member") + errMemberNumber = errors.New("too many list-members in baggage-string") + errMemberBytes = errors.New("list-member too large") + errBaggageBytes = errors.New("baggage-string too large") +) + +// Property is an additional metadata entry for a baggage list-member. +type Property struct { + key, value string + + // hasValue indicates if a zero-value value means the property does not + // have a value or if it was the zero-value. + hasValue bool + + // hasData indicates whether the created property contains data or not. + // Properties that do not contain data are invalid with no other check + // required. + hasData bool +} + +// NewKeyProperty returns a new Property for key. +// +// If key is invalid, an error will be returned. +func NewKeyProperty(key string) (Property, error) { + if !keyRe.MatchString(key) { + return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidKey, key) + } + + p := Property{key: key, hasData: true} + return p, nil +} + +// NewKeyValueProperty returns a new Property for key with value. +// +// If key or value are invalid, an error will be returned. +func NewKeyValueProperty(key, value string) (Property, error) { + if !keyRe.MatchString(key) { + return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidKey, key) + } + if !valueRe.MatchString(value) { + return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidValue, value) + } + + p := Property{ + key: key, + value: value, + hasValue: true, + hasData: true, + } + return p, nil +} + +func newInvalidProperty() Property { + return Property{} +} + +// parseProperty attempts to decode a Property from the passed string. It +// returns an error if the input is invalid according to the W3C Baggage +// specification. +func parseProperty(property string) (Property, error) { + if property == "" { + return newInvalidProperty(), nil + } + + match := propertyRe.FindStringSubmatch(property) + if len(match) != 4 { + return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidProperty, property) + } + + p := Property{hasData: true} + if match[1] != "" { + p.key = match[1] + } else { + p.key = match[2] + p.value = match[3] + p.hasValue = true + } + + return p, nil +} + +// validate ensures p conforms to the W3C Baggage specification, returning an +// error otherwise. +func (p Property) validate() error { + errFunc := func(err error) error { + return fmt.Errorf("invalid property: %w", err) + } + + if !p.hasData { + return errFunc(fmt.Errorf("%w: %q", errInvalidProperty, p)) + } + + if !keyRe.MatchString(p.key) { + return errFunc(fmt.Errorf("%w: %q", errInvalidKey, p.key)) + } + if p.hasValue && !valueRe.MatchString(p.value) { + return errFunc(fmt.Errorf("%w: %q", errInvalidValue, p.value)) + } + if !p.hasValue && p.value != "" { + return errFunc(errors.New("inconsistent value")) + } + return nil +} + +// Key returns the Property key. +func (p Property) Key() string { + return p.key +} + +// Value returns the Property value. Additionally, a boolean value is returned +// indicating if the returned value is the empty if the Property has a value +// that is empty or if the value is not set. +func (p Property) Value() (string, bool) { + return p.value, p.hasValue +} + +// String encodes Property into a string compliant with the W3C Baggage +// specification. +func (p Property) String() string { + if p.hasValue { + return fmt.Sprintf("%s%s%v", p.key, keyValueDelimiter, p.value) + } + return p.key +} + +type properties []Property + +func fromInternalProperties(iProps []baggage.Property) properties { + if len(iProps) == 0 { + return nil + } + + props := make(properties, len(iProps)) + for i, p := range iProps { + props[i] = Property{ + key: p.Key, + value: p.Value, + hasValue: p.HasValue, + } + } + return props +} + +func (p properties) asInternal() []baggage.Property { + if len(p) == 0 { + return nil + } + + iProps := make([]baggage.Property, len(p)) + for i, prop := range p { + iProps[i] = baggage.Property{ + Key: prop.key, + Value: prop.value, + HasValue: prop.hasValue, + } + } + return iProps +} + +func (p properties) Copy() properties { + if len(p) == 0 { + return nil + } + + props := make(properties, len(p)) + copy(props, p) + return props +} + +// validate ensures each Property in p conforms to the W3C Baggage +// specification, returning an error otherwise. +func (p properties) validate() error { + for _, prop := range p { + if err := prop.validate(); err != nil { + return err + } + } + return nil +} + +// String encodes properties into a string compliant with the W3C Baggage +// specification. +func (p properties) String() string { + props := make([]string, len(p)) + for i, prop := range p { + props[i] = prop.String() + } + return strings.Join(props, propertyDelimiter) +} + +// Member is a list-member of a baggage-string as defined by the W3C Baggage +// specification. +type Member struct { + key, value string + properties properties + + // hasData indicates whether the created property contains data or not. + // Properties that do not contain data are invalid with no other check + // required. + hasData bool +} + +// NewMember returns a new Member from the passed arguments. The key will be +// used directly while the value will be url decoded after validation. An error +// is returned if the created Member would be invalid according to the W3C +// Baggage specification. +func NewMember(key, value string, props ...Property) (Member, error) { + m := Member{ + key: key, + value: value, + properties: properties(props).Copy(), + hasData: true, + } + if err := m.validate(); err != nil { + return newInvalidMember(), err + } + decodedValue, err := url.QueryUnescape(value) + if err != nil { + return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidValue, value) + } + m.value = decodedValue + return m, nil +} + +func newInvalidMember() Member { + return Member{} +} + +// parseMember attempts to decode a Member from the passed string. It returns +// an error if the input is invalid according to the W3C Baggage +// specification. +func parseMember(member string) (Member, error) { + if n := len(member); n > maxBytesPerMembers { + return newInvalidMember(), fmt.Errorf("%w: %d", errMemberBytes, n) + } + + var ( + key, value string + props properties + ) + + parts := strings.SplitN(member, propertyDelimiter, 2) + switch len(parts) { + case 2: + // Parse the member properties. + for _, pStr := range strings.Split(parts[1], propertyDelimiter) { + p, err := parseProperty(pStr) + if err != nil { + return newInvalidMember(), err + } + props = append(props, p) + } + fallthrough + case 1: + // Parse the member key/value pair. + + // Take into account a value can contain equal signs (=). + kv := strings.SplitN(parts[0], keyValueDelimiter, 2) + if len(kv) != 2 { + return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidMember, member) + } + // "Leading and trailing whitespaces are allowed but MUST be trimmed + // when converting the header into a data structure." + key = strings.TrimSpace(kv[0]) + var err error + value, err = url.QueryUnescape(strings.TrimSpace(kv[1])) + if err != nil { + return newInvalidMember(), fmt.Errorf("%w: %q", err, value) + } + if !keyRe.MatchString(key) { + return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidKey, key) + } + if !valueRe.MatchString(value) { + return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidValue, value) + } + default: + // This should never happen unless a developer has changed the string + // splitting somehow. Panic instead of failing silently and allowing + // the bug to slip past the CI checks. + panic("failed to parse baggage member") + } + + return Member{key: key, value: value, properties: props, hasData: true}, nil +} + +// validate ensures m conforms to the W3C Baggage specification. +// A key is just an ASCII string, but a value must be URL encoded UTF-8, +// returning an error otherwise. +func (m Member) validate() error { + if !m.hasData { + return fmt.Errorf("%w: %q", errInvalidMember, m) + } + + if !keyRe.MatchString(m.key) { + return fmt.Errorf("%w: %q", errInvalidKey, m.key) + } + if !valueRe.MatchString(m.value) { + return fmt.Errorf("%w: %q", errInvalidValue, m.value) + } + return m.properties.validate() +} + +// Key returns the Member key. +func (m Member) Key() string { return m.key } + +// Value returns the Member value. +func (m Member) Value() string { return m.value } + +// Properties returns a copy of the Member properties. +func (m Member) Properties() []Property { return m.properties.Copy() } + +// String encodes Member into a string compliant with the W3C Baggage +// specification. +func (m Member) String() string { + // A key is just an ASCII string, but a value is URL encoded UTF-8. + s := fmt.Sprintf("%s%s%s", m.key, keyValueDelimiter, url.QueryEscape(m.value)) + if len(m.properties) > 0 { + s = fmt.Sprintf("%s%s%s", s, propertyDelimiter, m.properties.String()) + } + return s +} + +// Baggage is a list of baggage members representing the baggage-string as +// defined by the W3C Baggage specification. +type Baggage struct { //nolint:golint + list baggage.List +} + +// New returns a new valid Baggage. It returns an error if it results in a +// Baggage exceeding limits set in that specification. +// +// It expects all the provided members to have already been validated. +func New(members ...Member) (Baggage, error) { + if len(members) == 0 { + return Baggage{}, nil + } + + b := make(baggage.List) + for _, m := range members { + if !m.hasData { + return Baggage{}, errInvalidMember + } + + // OpenTelemetry resolves duplicates by last-one-wins. + b[m.key] = baggage.Item{ + Value: m.value, + Properties: m.properties.asInternal(), + } + } + + // Check member numbers after deduplication. + if len(b) > maxMembers { + return Baggage{}, errMemberNumber + } + + bag := Baggage{b} + if n := len(bag.String()); n > maxBytesPerBaggageString { + return Baggage{}, fmt.Errorf("%w: %d", errBaggageBytes, n) + } + + return bag, nil +} + +// Parse attempts to decode a baggage-string from the passed string. It +// returns an error if the input is invalid according to the W3C Baggage +// specification. +// +// If there are duplicate list-members contained in baggage, the last one +// defined (reading left-to-right) will be the only one kept. This diverges +// from the W3C Baggage specification which allows duplicate list-members, but +// conforms to the OpenTelemetry Baggage specification. +func Parse(bStr string) (Baggage, error) { + if bStr == "" { + return Baggage{}, nil + } + + if n := len(bStr); n > maxBytesPerBaggageString { + return Baggage{}, fmt.Errorf("%w: %d", errBaggageBytes, n) + } + + b := make(baggage.List) + for _, memberStr := range strings.Split(bStr, listDelimiter) { + m, err := parseMember(memberStr) + if err != nil { + return Baggage{}, err + } + // OpenTelemetry resolves duplicates by last-one-wins. + b[m.key] = baggage.Item{ + Value: m.value, + Properties: m.properties.asInternal(), + } + } + + // OpenTelemetry does not allow for duplicate list-members, but the W3C + // specification does. Now that we have deduplicated, ensure the baggage + // does not exceed list-member limits. + if len(b) > maxMembers { + return Baggage{}, errMemberNumber + } + + return Baggage{b}, nil +} + +// Member returns the baggage list-member identified by key. +// +// If there is no list-member matching the passed key the returned Member will +// be a zero-value Member. +// The returned member is not validated, as we assume the validation happened +// when it was added to the Baggage. +func (b Baggage) Member(key string) Member { + v, ok := b.list[key] + if !ok { + // We do not need to worry about distinguishing between the situation + // where a zero-valued Member is included in the Baggage because a + // zero-valued Member is invalid according to the W3C Baggage + // specification (it has an empty key). + return newInvalidMember() + } + + return Member{ + key: key, + value: v.Value, + properties: fromInternalProperties(v.Properties), + hasData: true, + } +} + +// Members returns all the baggage list-members. +// The order of the returned list-members does not have significance. +// +// The returned members are not validated, as we assume the validation happened +// when they were added to the Baggage. +func (b Baggage) Members() []Member { + if len(b.list) == 0 { + return nil + } + + members := make([]Member, 0, len(b.list)) + for k, v := range b.list { + members = append(members, Member{ + key: k, + value: v.Value, + properties: fromInternalProperties(v.Properties), + hasData: true, + }) + } + return members +} + +// SetMember returns a copy the Baggage with the member included. If the +// baggage contains a Member with the same key the existing Member is +// replaced. +// +// If member is invalid according to the W3C Baggage specification, an error +// is returned with the original Baggage. +func (b Baggage) SetMember(member Member) (Baggage, error) { + if !member.hasData { + return b, errInvalidMember + } + + n := len(b.list) + if _, ok := b.list[member.key]; !ok { + n++ + } + list := make(baggage.List, n) + + for k, v := range b.list { + // Do not copy if we are just going to overwrite. + if k == member.key { + continue + } + list[k] = v + } + + list[member.key] = baggage.Item{ + Value: member.value, + Properties: member.properties.asInternal(), + } + + return Baggage{list: list}, nil +} + +// DeleteMember returns a copy of the Baggage with the list-member identified +// by key removed. +func (b Baggage) DeleteMember(key string) Baggage { + n := len(b.list) + if _, ok := b.list[key]; ok { + n-- + } + list := make(baggage.List, n) + + for k, v := range b.list { + if k == key { + continue + } + list[k] = v + } + + return Baggage{list: list} +} + +// Len returns the number of list-members in the Baggage. +func (b Baggage) Len() int { + return len(b.list) +} + +// String encodes Baggage into a string compliant with the W3C Baggage +// specification. The returned string will be invalid if the Baggage contains +// any invalid list-members. +func (b Baggage) String() string { + members := make([]string, 0, len(b.list)) + for k, v := range b.list { + members = append(members, Member{ + key: k, + value: v.Value, + properties: fromInternalProperties(v.Properties), + }.String()) + } + return strings.Join(members, listDelimiter) +} diff --git a/internal/otel/baggage/internal/baggage/baggage.go b/internal/otel/baggage/internal/baggage/baggage.go new file mode 100644 index 00000000..77ff7e28 --- /dev/null +++ b/internal/otel/baggage/internal/baggage/baggage.go @@ -0,0 +1,45 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* +Package baggage provides base types and functionality to store and retrieve +baggage in Go context. This package exists because the OpenTracing bridge to +OpenTelemetry needs to synchronize state whenever baggage for a context is +modified and that context contains an OpenTracing span. If it were not for +this need this package would not need to exist and the +`go.opentelemetry.io/otel/baggage` package would be the singular place where +W3C baggage is handled. +*/ + +// From from https://github.com/open-telemetry/opentelemetry-go/blob/main/internal/baggage/baggage.go +package baggage + +// List is the collection of baggage members. The W3C allows for duplicates, +// but OpenTelemetry does not, therefore, this is represented as a map. +type List map[string]Item + +// Item is the value and metadata properties part of a list-member. +type Item struct { + Value string + Properties []Property +} + +// Property is a metadata entry for a list-member. +type Property struct { + Key, Value string + + // HasValue indicates if a zero-value value means the property does not + // have a value or if it was the zero-value. + HasValue bool +} diff --git a/tracing.go b/tracing.go index 118699b0..bab1bbdd 100644 --- a/tracing.go +++ b/tracing.go @@ -34,6 +34,9 @@ type Span struct { //nolint: maligned // prefer readability over optimal memory // ctx is the context where the span was started. Always non-nil. ctx context.Context + // Dynamic Sampling context + dynamicSamplingContext DynamicSamplingContext + // parent refers to the immediate local parent span. A remote parent span is // only referenced by setting ParentSpanID. parent *Span @@ -226,6 +229,14 @@ func (s *Span) ToSentryTrace() string { return b.String() } +func (s *Span) ToBaggage() string { + if dsc := s.dynamicSamplingContext; len(dsc.Entries) > 0 { + return dsc.String() + } + + return "" +} + // sentryTracePattern matches either // // TRACE_ID - SPAN_ID @@ -258,6 +269,17 @@ func (s *Span) updateFromSentryTrace(header []byte) { } } +func (s *Span) updateFromBaggage(header []byte) { + if s.isTransaction { + dsc, err := NewDynamicSamplingContext(header) + if err != nil { + return + } + + s.dynamicSamplingContext = dsc + } +} + func (s *Span) MarshalJSON() ([]byte, error) { // span aliases Span to allow calling json.Marshal without an infinite loop. // It preserves all fields while none of the attached methods. @@ -337,6 +359,9 @@ func (s *Span) toEvent() *Event { Timestamp: s.EndTime, StartTime: s.StartTime, Spans: finished, + SDKMetaData: SDKMetaData{ + DynamicSamplingContextKey: s.dynamicSamplingContext, + }, } } @@ -580,9 +605,22 @@ func OpName(name string) SpanOption { // // ContinueFromRequest is an alias for: // -// ContinueFromTrace(r.Header.Get("sentry-trace")) +// ContinueFromHeaders(r.Header.Get("sentry-trace"), r.Header.Get("baggage")) func ContinueFromRequest(r *http.Request) SpanOption { - return ContinueFromTrace(r.Header.Get("sentry-trace")) + return ContinueFromHeaders(r.Header.Get("sentry-trace"), r.Header.Get("baggage")) +} + +// ContinueFromHeaders returns a span option that updates the span to continue +// an existing TraceID and propagates the Dynamic Sampling context. +func ContinueFromHeaders(trace string, baggage string) SpanOption { + return func(s *Span) { + if trace != "" { + s.updateFromSentryTrace([]byte(trace)) + } + if baggage != "" { + s.updateFromBaggage([]byte(baggage)) + } + } } // ContinueFromTrace returns a span option that updates the span to continue diff --git a/transport.go b/transport.go index d8fa2be5..0e9a19ad 100644 --- a/transport.go +++ b/transport.go @@ -94,21 +94,36 @@ func getRequestBodyFromEvent(event *Event) []byte { return nil } -func transactionEnvelopeFromBody(eventID EventID, sentAt time.Time, body json.RawMessage) (*bytes.Buffer, error) { +func transactionEnvelopeFromBody(event *Event, sentAt time.Time, body json.RawMessage) (*bytes.Buffer, error) { var b bytes.Buffer enc := json.NewEncoder(&b) - // envelope header + + dsc := event.SDKMetaData.DynamicSamplingContextKey + var trace = map[string]string{} + + for k, v := range dsc.Entries { + trace[k] = v + } + + // Envelope header + // + // TODO + // - add DSN + // - add SDK map[string]string{"name": "sentry.go", "version": Version} err := enc.Encode(struct { - EventID EventID `json:"event_id"` - SentAt time.Time `json:"sent_at"` + EventID EventID `json:"event_id"` + SentAt time.Time `json:"sent_at"` + Trace map[string]string `json:"trace,omitempty"` }{ - EventID: eventID, + EventID: event.EventID, SentAt: sentAt, + Trace: trace, }) if err != nil { return nil, err } - // item header + + // Item header err = enc.Encode(struct { Type string `json:"type"` Length int `json:"length"` @@ -124,6 +139,7 @@ func transactionEnvelopeFromBody(eventID EventID, sentAt time.Time, body json.Ra if err != nil { return nil, err } + return &b, nil } @@ -138,7 +154,7 @@ func getRequestFromEvent(event *Event, dsn *Dsn) (r *http.Request, err error) { return nil, errors.New("event could not be marshaled") } if event.Type == transactionType { - b, err := transactionEnvelopeFromBody(event.EventID, time.Now(), body) + b, err := transactionEnvelopeFromBody(event, time.Now(), body) if err != nil { return nil, err } diff --git a/transport_test.go b/transport_test.go index 82d2f170..5cc4e526 100644 --- a/transport_test.go +++ b/transport_test.go @@ -134,9 +134,11 @@ func TestGetRequestBodyFromEventCompletelyInvalid(t *testing.T) { func TestTransactionEnvelopeFromBody(t *testing.T) { const eventID = "b81c5be4d31e48959103a1f878a1efcb" + event := NewEvent() + event.EventID = eventID sentAt := time.Unix(0, 0).UTC() body := json.RawMessage(`{"type":"transaction","fields":"omitted"}`) - b, err := transactionEnvelopeFromBody(eventID, sentAt, body) + b, err := transactionEnvelopeFromBody(event, sentAt, body) if err != nil { t.Fatal(err) } From 6e3de1bce45e53ecb759634cbc0d3d62cc52f539 Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Mon, 7 Nov 2022 10:00:55 +0100 Subject: [PATCH 02/16] Fix typo --- internal/otel/baggage/internal/baggage/baggage.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/otel/baggage/internal/baggage/baggage.go b/internal/otel/baggage/internal/baggage/baggage.go index 77ff7e28..36a84143 100644 --- a/internal/otel/baggage/internal/baggage/baggage.go +++ b/internal/otel/baggage/internal/baggage/baggage.go @@ -22,7 +22,7 @@ this need this package would not need to exist and the W3C baggage is handled. */ -// From from https://github.com/open-telemetry/opentelemetry-go/blob/main/internal/baggage/baggage.go +// From https://github.com/open-telemetry/opentelemetry-go/blob/main/internal/baggage/baggage.go package baggage // List is the collection of baggage members. The W3C allows for duplicates, From 12cb9e43aa58ea0da236eed0070a2ccd17df8d33 Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Mon, 7 Nov 2022 10:01:30 +0100 Subject: [PATCH 03/16] Add DSN & SDK envelope headers --- transport.go | 15 +++++++++------ transport_test.go | 16 ++++++++++++++-- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/transport.go b/transport.go index 0e9a19ad..f9168d1c 100644 --- a/transport.go +++ b/transport.go @@ -94,7 +94,7 @@ func getRequestBodyFromEvent(event *Event) []byte { return nil } -func transactionEnvelopeFromBody(event *Event, sentAt time.Time, body json.RawMessage) (*bytes.Buffer, error) { +func transactionEnvelopeFromBody(event *Event, dsn *Dsn, sentAt time.Time, body json.RawMessage) (*bytes.Buffer, error) { var b bytes.Buffer enc := json.NewEncoder(&b) @@ -106,18 +106,21 @@ func transactionEnvelopeFromBody(event *Event, sentAt time.Time, body json.RawMe } // Envelope header - // - // TODO - // - add DSN - // - add SDK map[string]string{"name": "sentry.go", "version": Version} err := enc.Encode(struct { EventID EventID `json:"event_id"` SentAt time.Time `json:"sent_at"` + Dsn string `json:"dsn"` + Sdk map[string]string `json:"sdk"` Trace map[string]string `json:"trace,omitempty"` }{ EventID: event.EventID, SentAt: sentAt, Trace: trace, + Dsn: dsn.String(), + Sdk: map[string]string{ + "name": event.Sdk.Name, + "version": event.Sdk.Version, + }, }) if err != nil { return nil, err @@ -154,7 +157,7 @@ func getRequestFromEvent(event *Event, dsn *Dsn) (r *http.Request, err error) { return nil, errors.New("event could not be marshaled") } if event.Type == transactionType { - b, err := transactionEnvelopeFromBody(event, time.Now(), body) + b, err := transactionEnvelopeFromBody(event, dsn, time.Now(), body) if err != nil { return nil, err } diff --git a/transport_test.go b/transport_test.go index 5cc4e526..5557c064 100644 --- a/transport_test.go +++ b/transport_test.go @@ -136,14 +136,26 @@ func TestTransactionEnvelopeFromBody(t *testing.T) { const eventID = "b81c5be4d31e48959103a1f878a1efcb" event := NewEvent() event.EventID = eventID + event.Sdk = SdkInfo{ + Name: "sentry.go", + Version: "0.0.1", + } + + dsn, err := NewDsn("http://public@example.com/sentry/1") + if err != nil { + t.Fatal(err) + } + sentAt := time.Unix(0, 0).UTC() + body := json.RawMessage(`{"type":"transaction","fields":"omitted"}`) - b, err := transactionEnvelopeFromBody(event, sentAt, body) + + b, err := transactionEnvelopeFromBody(event, dsn, sentAt, body) if err != nil { t.Fatal(err) } got := b.String() - want := `{"event_id":"b81c5be4d31e48959103a1f878a1efcb","sent_at":"1970-01-01T00:00:00Z"} + want := `{"event_id":"b81c5be4d31e48959103a1f878a1efcb","sent_at":"1970-01-01T00:00:00Z","dsn":"http://public@example.com/sentry/1","sdk":{"name":"sentry.go","version":"0.0.1"}} {"type":"transaction","length":41} {"type":"transaction","fields":"omitted"} ` From 565c4a650045bf1a7e3cd2ad5b08ee847100a2fc Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Mon, 7 Nov 2022 10:01:43 +0100 Subject: [PATCH 04/16] Add SDKIdentifier --- client.go | 6 +++--- client_test.go | 4 ++-- sentry.go | 10 ++++++++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/client.go b/client.go index 965b3e75..a886a267 100644 --- a/client.go +++ b/client.go @@ -624,12 +624,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, }}, } diff --git a/client_test.go b/client_test.go index 88a98824..eb64d3db 100644 --- a/client_test.go +++ b/client_test.go @@ -266,14 +266,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 diff --git a/sentry.go b/sentry.go index 9564a7e1..b6f90da9 100644 --- a/sentry.go +++ b/sentry.go @@ -5,15 +5,21 @@ import ( "time" ) +// Deprecated: Use SDKVersion instead. +const Version = SDKVersion + // Version is the version of the SDK. -const Version = "0.14.0" +const SDKVersion = "0.14.0" + +// The identifier of the SDK. +const SDKIdentifier = "sentry.go" // apiVersion is the minimum version of the Sentry API compatible with the // sentry-go SDK. const apiVersion = "7" // userAgent is the User-Agent of outgoing HTTP requests. -const userAgent = "sentry-go/" + Version +const userAgent = "sentry-go/" + SDKVersion // Init initializes the SDK with options. The returned error is non-nil if // options is invalid, for instance if a malformed DSN is provided. From df61c9f143a2e74a0a34c7d79d8a44399881d74c Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Mon, 7 Nov 2022 15:19:14 +0100 Subject: [PATCH 05/16] Review feedback --- interfaces.go | 2 +- tracing.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/interfaces.go b/interfaces.go index c082ab38..9658e702 100644 --- a/interfaces.go +++ b/interfaces.go @@ -223,7 +223,7 @@ const ( // SDKMetaData is a struct to stash data which is needed at some point in the SDK's event processing pipeline // but which shouldn't get send to Sentry. -type SDKMetaData = struct { +type SDKMetaData struct { DynamicSamplingContextKey DynamicSamplingContext } diff --git a/tracing.go b/tracing.go index bab1bbdd..b5a4e199 100644 --- a/tracing.go +++ b/tracing.go @@ -230,8 +230,8 @@ func (s *Span) ToSentryTrace() string { } func (s *Span) ToBaggage() string { - if dsc := s.dynamicSamplingContext; len(dsc.Entries) > 0 { - return dsc.String() + if len(s.dynamicSamplingContext.Entries) > 0 { + return s.dynamicSamplingContext.String() } return "" @@ -612,7 +612,7 @@ func ContinueFromRequest(r *http.Request) SpanOption { // ContinueFromHeaders returns a span option that updates the span to continue // an existing TraceID and propagates the Dynamic Sampling context. -func ContinueFromHeaders(trace string, baggage string) SpanOption { +func ContinueFromHeaders(trace, baggage string) SpanOption { return func(s *Span) { if trace != "" { s.updateFromSentryTrace([]byte(trace)) From 7ecd8146cf7b777424e0b60b670c55fb12fefb23 Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Mon, 7 Nov 2022 15:43:14 +0100 Subject: [PATCH 06/16] =?UTF-8?q?Don=E2=80=99t=20export=20sdkMetaData?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client_test.go | 47 ++++++++++++++++++++------------- fasthttp/sentryfasthttp_test.go | 1 + http/sentryhttp_test.go | 1 + interfaces.go | 4 +-- tracing.go | 2 +- tracing_test.go | 3 +++ transport.go | 2 +- 7 files changed, 38 insertions(+), 22 deletions(-) diff --git a/client_test.go b/client_test.go index eb64d3db..d7d54d3a 100644 --- a/client_test.go +++ b/client_test.go @@ -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) } @@ -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) } @@ -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) } @@ -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) } diff --git a/fasthttp/sentryfasthttp_test.go b/fasthttp/sentryfasthttp_test.go index 0c718636..3ee8b666 100644 --- a/fasthttp/sentryfasthttp_test.go +++ b/fasthttp/sentryfasthttp_test.go @@ -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 diff --git a/http/sentryhttp_test.go b/http/sentryhttp_test.go index 9b8efb54..2d97ce8b 100644 --- a/http/sentryhttp_test.go +++ b/http/sentryhttp_test.go @@ -216,6 +216,7 @@ func TestIntegration(t *testing.T) { sentry.Event{}, "Contexts", "EventID", "Extra", "Platform", "Modules", "Release", "Sdk", "ServerName", "Tags", "Timestamp", + "sdkMetaData", ), cmpopts.IgnoreFields( sentry.Request{}, diff --git a/interfaces.go b/interfaces.go index 9658e702..341f8aa7 100644 --- a/interfaces.go +++ b/interfaces.go @@ -221,7 +221,7 @@ const ( DynamicSamplingContextKey SDKMetaDataKey = "DynamicSamplingContext" ) -// SDKMetaData is a struct to stash data which is needed at some point in the SDK's event processing pipeline +// sdkMetaData is a struct to stash data which is needed at some point in the SDK's event processing pipeline // but which shouldn't get send to Sentry. type SDKMetaData struct { DynamicSamplingContextKey DynamicSamplingContext @@ -266,7 +266,7 @@ type Event struct { // The fields below are not part of the final JSON payload. - SDKMetaData SDKMetaData `json:"-"` + sdkMetaData SDKMetaData } // TODO: Event.Contexts map[string]interface{} => map[string]EventContext, diff --git a/tracing.go b/tracing.go index b5a4e199..f8231660 100644 --- a/tracing.go +++ b/tracing.go @@ -359,7 +359,7 @@ func (s *Span) toEvent() *Event { Timestamp: s.EndTime, StartTime: s.StartTime, Spans: finished, - SDKMetaData: SDKMetaData{ + sdkMetaData: SDKMetaData{ DynamicSamplingContextKey: s.dynamicSamplingContext, }, } diff --git a/tracing_test.go b/tracing_test.go index 347a6dde..e012b763 100644 --- a/tracing_test.go +++ b/tracing_test.go @@ -152,6 +152,7 @@ func TestStartSpan(t *testing.T) { cmpopts.IgnoreFields(Event{}, "Contexts", "EventID", "Level", "Platform", "Release", "Sdk", "ServerName", "Modules", + "sdkMetaData", ), cmpopts.EquateEmpty(), } @@ -211,6 +212,7 @@ func TestStartChild(t *testing.T) { cmpopts.IgnoreFields(Event{}, "EventID", "Level", "Platform", "Modules", "Release", "Sdk", "ServerName", "Timestamp", "StartTime", + "sdkMetaData", ), cmpopts.IgnoreMapEntries(func(k string, v interface{}) bool { return k != "trace" @@ -285,6 +287,7 @@ func TestStartTransaction(t *testing.T) { cmpopts.IgnoreFields(Event{}, "Contexts", "EventID", "Level", "Platform", "Release", "Sdk", "ServerName", "Modules", + "sdkMetaData", ), cmpopts.EquateEmpty(), } diff --git a/transport.go b/transport.go index f9168d1c..05bcd3c7 100644 --- a/transport.go +++ b/transport.go @@ -98,7 +98,7 @@ func transactionEnvelopeFromBody(event *Event, dsn *Dsn, sentAt time.Time, body var b bytes.Buffer enc := json.NewEncoder(&b) - dsc := event.SDKMetaData.DynamicSamplingContextKey + dsc := event.sdkMetaData.DynamicSamplingContextKey var trace = map[string]string{} for k, v := range dsc.Entries { From 6d4ca42a1799c3ac8058070be54088d96672448a Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Tue, 8 Nov 2022 17:09:12 +0100 Subject: [PATCH 07/16] Use simle struct field --- interfaces.go | 10 ++-------- tracing.go | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/interfaces.go b/interfaces.go index 341f8aa7..01de6c01 100644 --- a/interfaces.go +++ b/interfaces.go @@ -215,16 +215,10 @@ type Exception struct { Stacktrace *Stacktrace `json:"stacktrace,omitempty"` } -type SDKMetaDataKey = string - -const ( - DynamicSamplingContextKey SDKMetaDataKey = "DynamicSamplingContext" -) - -// sdkMetaData is a struct to stash data which is needed at some point in the SDK's event processing pipeline +// SDKMetaData is a struct to stash data which is needed at some point in the SDK's event processing pipeline // but which shouldn't get send to Sentry. type SDKMetaData struct { - DynamicSamplingContextKey DynamicSamplingContext + dsc DynamicSamplingContext } // EventID is a hexadecimal string representing a unique uuid4 for an Event. diff --git a/tracing.go b/tracing.go index f8231660..a6e2f2c6 100644 --- a/tracing.go +++ b/tracing.go @@ -360,7 +360,7 @@ func (s *Span) toEvent() *Event { StartTime: s.StartTime, Spans: finished, sdkMetaData: SDKMetaData{ - DynamicSamplingContextKey: s.dynamicSamplingContext, + dsc: s.dynamicSamplingContext, }, } } From e0830e9172d7b1b440b718c288c663a7c056d8aa Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Tue, 8 Nov 2022 17:11:11 +0100 Subject: [PATCH 08/16] Use permalinks --- internal/otel/baggage/baggage.go | 6 ++++-- internal/otel/baggage/internal/baggage/baggage.go | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/internal/otel/baggage/baggage.go b/internal/otel/baggage/baggage.go index 384a916a..16e8ed0c 100644 --- a/internal/otel/baggage/baggage.go +++ b/internal/otel/baggage/baggage.go @@ -1,4 +1,7 @@ -// Copyright The OpenTelemetry Authors +// This file was vendored in unmodified from +// https://github.com/open-telemetry/opentelemetry-go/blob/c21b6b6bb31a2f74edd06e262f1690f3f6ea3d5c/baggage/baggage.go +// +// # Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,7 +15,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// From https://github.com/open-telemetry/opentelemetry-go/blob/main/baggage/baggage.go package baggage import ( diff --git a/internal/otel/baggage/internal/baggage/baggage.go b/internal/otel/baggage/internal/baggage/baggage.go index 36a84143..04e41402 100644 --- a/internal/otel/baggage/internal/baggage/baggage.go +++ b/internal/otel/baggage/internal/baggage/baggage.go @@ -1,3 +1,6 @@ +// This file was vendored in unmodified from +// https://github.com/open-telemetry/opentelemetry-go/blob/c21b6b6bb31a2f74edd06e262f1690f3f6ea3d5c/internal/baggage/baggage.go +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,8 +24,6 @@ this need this package would not need to exist and the `go.opentelemetry.io/otel/baggage` package would be the singular place where W3C baggage is handled. */ - -// From https://github.com/open-telemetry/opentelemetry-go/blob/main/internal/baggage/baggage.go package baggage // List is the collection of baggage members. The W3C allows for duplicates, From bb4cb2ca5e668f7a9abc67682379ed6f950eaa85 Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Tue, 8 Nov 2022 17:18:07 +0100 Subject: [PATCH 09/16] Adapt craft --- .craft.yml | 3 +-- scripts/bump-version.sh | 24 ++++++++++++++++++++++++ scripts/craft-pre-release.sh | 24 ------------------------ 3 files changed, 25 insertions(+), 26 deletions(-) create mode 100755 scripts/bump-version.sh delete mode 100755 scripts/craft-pre-release.sh diff --git a/.craft.yml b/.craft.yml index 5f786f52..4af175ad 100644 --- a/.craft.yml +++ b/.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: diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh new file mode 100755 index 00000000..9f4cb447 --- /dev/null +++ b/scripts/bump-version.sh @@ -0,0 +1,24 @@ +#!/bin/bash +set -eux + +if [ "$(uname -s)" != "Linux" ]; then + echo "Please use the GitHub Action." + exit 1 +fi + +SCRIPT_DIR="$( dirname "$0" )" +cd $SCRIPT_DIR/.. + +OLD_VERSION="${1}" +NEW_VERSION="${2}" + +echo "Current version: $OLD_VERSION" +echo "Bumping version: $NEW_VERSION" + +function replace() { + ! grep "$2" $3 + perl -i -pe "s/$1/$2/g" $3 + grep "$2" $3 # verify that replacement was successful +} + +replace "const SDKVersion = \"[\w.-]+\"" "const SDKVersion = \"$NEW_VERSION\"" ./sentry.go diff --git a/scripts/craft-pre-release.sh b/scripts/craft-pre-release.sh deleted file mode 100755 index feb507d1..00000000 --- a/scripts/craft-pre-release.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -set -eux - -SCRIPT_DIR="$( dirname "$0" )" -cd $SCRIPT_DIR/.. - -function replace() { - ! grep "$2" $3 - perl -i -pe "s/$1/$2/g" $3 - grep "$2" $3 # verify that replacement was successful -} - -if [ "$#" -eq 1 ]; then - OLD_VERSION="" - NEW_VERSION="${1}" -elif [ "$#" -eq 2 ]; then - OLD_VERSION="${1}" - NEW_VERSION="${2}" -else - echo "Illegal number of parameters" - exit 1 -fi - -replace "const Version = \"[\w.-]+\"" "const Version = \"$NEW_VERSION\"" ./sentry.go From 2338b6506a136c35bd90d3f413604e7d52bf38e2 Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Tue, 8 Nov 2022 17:24:34 +0100 Subject: [PATCH 10/16] Fix tests --- transport.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transport.go b/transport.go index 05bcd3c7..6661f6a2 100644 --- a/transport.go +++ b/transport.go @@ -98,7 +98,7 @@ func transactionEnvelopeFromBody(event *Event, dsn *Dsn, sentAt time.Time, body var b bytes.Buffer enc := json.NewEncoder(&b) - dsc := event.sdkMetaData.DynamicSamplingContextKey + dsc := event.sdkMetaData.dsc var trace = map[string]string{} for k, v := range dsc.Entries { From adc14fb293b9843aa572e96f7e1ae03744be735c Mon Sep 17 00:00:00 2001 From: Michi Hoffmann Date: Wed, 9 Nov 2022 16:50:29 +0100 Subject: [PATCH 11/16] Update tracing.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kamil Ogórek --- tracing.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tracing.go b/tracing.go index a6e2f2c6..0e59fd28 100644 --- a/tracing.go +++ b/tracing.go @@ -230,7 +230,7 @@ func (s *Span) ToSentryTrace() string { } func (s *Span) ToBaggage() string { - if len(s.dynamicSamplingContext.Entries) > 0 { + if s.dynamicSamplingContext.HasEntries() { return s.dynamicSamplingContext.String() } From 135e56b04a6602dcc1e395f48ce4d62dc9f6e94d Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Wed, 9 Nov 2022 16:51:39 +0100 Subject: [PATCH 12/16] Add TODO comment --- dynamic_sampling_context.go | 1 + 1 file changed, 1 insertion(+) diff --git a/dynamic_sampling_context.go b/dynamic_sampling_context.go index 95a5307c..a9c9d6b8 100644 --- a/dynamic_sampling_context.go +++ b/dynamic_sampling_context.go @@ -41,5 +41,6 @@ func (d DynamicSamplingContext) HasEntries() bool { } func (d DynamicSamplingContext) String() string { + // TODO implement me return "" } From d01ce467eb6942b653780c9dfe1681f464a5070c Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Tue, 29 Nov 2022 23:22:41 +0100 Subject: [PATCH 13/16] Fix logrus tests --- logrus/logrusentry_test.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/logrus/logrusentry_test.go b/logrus/logrusentry_test.go index 5e30e55e..5975ab06 100644 --- a/logrus/logrusentry_test.go +++ b/logrus/logrusentry_test.go @@ -12,6 +12,7 @@ import ( "time" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" pkgerr "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -399,7 +400,12 @@ func Test_entry2event(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() got := h.entryToEvent(tt.entry) - if d := cmp.Diff(tt.want, got); d != "" { + opts := cmp.Options{ + cmpopts.IgnoreFields(sentry.Event{}, + "sdkMetaData", + ), + } + if d := cmp.Diff(tt.want, got, opts); d != "" { t.Error(d) } }) From f219a81773df9d970be0b71bed39bc1f1af71268 Mon Sep 17 00:00:00 2001 From: Michi Hoffmann Date: Wed, 30 Nov 2022 13:55:05 +0100 Subject: [PATCH 14/16] [Dynamic Sampling] Head of trace (#492) Co-authored-by: Abhijeet Prasad --- dynamic_sampling_context.go | 51 ++++++++++++++++++- dynamic_sampling_context_test.go | 84 +++++++++++++++++++++++++++++++- http/sentryhttp.go | 7 +-- interfaces.go | 3 +- tracing.go | 58 ++++++++++++++++++---- 5 files changed, 186 insertions(+), 17 deletions(-) diff --git a/dynamic_sampling_context.go b/dynamic_sampling_context.go index a9c9d6b8..1cdab28f 100644 --- a/dynamic_sampling_context.go +++ b/dynamic_sampling_context.go @@ -1,6 +1,7 @@ package sentry import ( + "strconv" "strings" "github.com/getsentry/sentry-go/internal/otel/baggage" @@ -16,7 +17,7 @@ type DynamicSamplingContext struct { Frozen bool } -func NewDynamicSamplingContext(header []byte) (DynamicSamplingContext, error) { +func DynamicSamplingContextFromHeader(header []byte) (DynamicSamplingContext, error) { bag, err := baggage.Parse(string(header)) if err != nil { return DynamicSamplingContext{}, err @@ -36,10 +37,58 @@ func NewDynamicSamplingContext(header []byte) (DynamicSamplingContext, error) { }, 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 { // TODO implement me return "" diff --git a/dynamic_sampling_context_test.go b/dynamic_sampling_context_test.go index 0be16d55..ca8bec70 100644 --- a/dynamic_sampling_context_test.go +++ b/dynamic_sampling_context_test.go @@ -4,7 +4,7 @@ import ( "testing" ) -func TestNewDynamicSamplingContext(t *testing.T) { +func TestDynamicSamplingContextFromHeader(t *testing.T) { tests := []struct { input []byte want DynamicSamplingContext @@ -41,10 +41,90 @@ func TestNewDynamicSamplingContext(t *testing.T) { } for _, tc := range tests { - got, err := NewDynamicSamplingContext(tc.input) + 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") + 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) +} diff --git a/http/sentryhttp.go b/http/sentryhttp.go index 86c5df38..10515310 100644 --- a/http/sentryhttp.go +++ b/http/sentryhttp.go @@ -71,9 +71,9 @@ func (h *Handler) Handle(handler http.Handler) http.Handler { // where that is convenient. In particular, use it to wrap a handler function // literal. // -// http.Handle(pattern, h.HandleFunc(func (w http.ResponseWriter, r *http.Request) { -// // handler code here -// })) +// http.Handle(pattern, h.HandleFunc(func (w http.ResponseWriter, r *http.Request) { +// // handler code here +// })) func (h *Handler) HandleFunc(handler http.HandlerFunc) http.HandlerFunc { return h.handle(handler) } @@ -89,6 +89,7 @@ func (h *Handler) handle(handler http.Handler) http.HandlerFunc { options := []sentry.SpanOption{ sentry.OpName("http.server"), sentry.ContinueFromRequest(r), + sentry.TransctionSource(sentry.SourceURL), } // We don't mind getting an existing transaction back so we don't need to // check if it is. diff --git a/interfaces.go b/interfaces.go index 01de6c01..5adf1251 100644 --- a/interfaces.go +++ b/interfaces.go @@ -218,7 +218,8 @@ type Exception struct { // SDKMetaData is a struct to stash data which is needed at some point in the SDK's event processing pipeline // but which shouldn't get send to Sentry. type SDKMetaData struct { - dsc DynamicSamplingContext + dsc DynamicSamplingContext + transactionSource TransactionSource } // EventID is a hexadecimal string representing a unique uuid4 for an Event. diff --git a/tracing.go b/tracing.go index 6023cd9d..33b4e5aa 100644 --- a/tracing.go +++ b/tracing.go @@ -28,25 +28,23 @@ type Span struct { //nolint: maligned // prefer readability over optimal memory StartTime time.Time `json:"start_timestamp"` EndTime time.Time `json:"timestamp"` Data map[string]interface{} `json:"data,omitempty"` + Sampled Sampled `json:"-"` + Source TransactionSource `json:"-"` - Sampled Sampled `json:"-"` - + // sample rate the span was sampled with. + sampleRate float64 // ctx is the context where the span was started. Always non-nil. ctx context.Context - // Dynamic Sampling context dynamicSamplingContext DynamicSamplingContext - // parent refers to the immediate local parent span. A remote parent span is // only referenced by setting ParentSpanID. parent *Span - // isTransaction is true only for the root span of a local span tree. The // root span is the first span started in a context. Note that a local root // span may have a remote parent belonging to the same trace, therefore // isTransaction depends on ctx and not on parent. isTransaction bool - // recorder stores all spans in a transaction. Guaranteed to be non-nil. recorder *spanRecorder } @@ -141,7 +139,6 @@ func StartSpan(ctx context.Context, operation string, options ...SpanOption) *Sp option(&span) } - // TODO only sample transactions? span.Sampled = span.sample() if hasParent { @@ -273,7 +270,7 @@ func (s *Span) updateFromSentryTrace(header []byte) { func (s *Span) updateFromBaggage(header []byte) { if s.isTransaction { - dsc, err := NewDynamicSamplingContext(header) + dsc, err := DynamicSamplingContextFromHeader(header) if err != nil { return } @@ -311,12 +308,19 @@ func (s *Span) sample() Sampled { // #1 tracing is not enabled. if !clientOptions.EnableTracing { Logger.Printf("Dropping transaction: EnableTracing is set to %t", clientOptions.EnableTracing) + s.sampleRate = 0.0 return SampledFalse } // #2 explicit sampling decision via StartSpan/StartTransaction options. if s.Sampled != SampledUndefined { Logger.Printf("Using explicit sampling decision from StartSpan/StartTransaction: %v", s.Sampled) + switch s.Sampled { + case SampledTrue: + s.sampleRate = 1.0 + case SampledFalse: + s.sampleRate = 0.0 + } return s.Sampled } @@ -333,6 +337,7 @@ func (s *Span) sample() Sampled { samplingContext := SamplingContext{Span: s, Parent: s.parent} if sampler != nil { tracesSamplerSampleRate := sampler.Sample(samplingContext) + s.sampleRate = tracesSamplerSampleRate if tracesSamplerSampleRate < 0.0 || tracesSamplerSampleRate > 1.0 { Logger.Printf("Dropping transaction: Returned TracesSampler rate is out of range [0.0, 1.0]: %f", tracesSamplerSampleRate) return SampledFalse @@ -351,11 +356,18 @@ func (s *Span) sample() Sampled { // #4 inherit parent decision. if s.parent != nil { Logger.Printf("Using sampling decision from parent: %v", s.parent.Sampled) + switch s.parent.Sampled { + case SampledTrue: + s.sampleRate = 1.0 + case SampledFalse: + s.sampleRate = 0.0 + } return s.parent.Sampled } // #5 use TracesSampleRate from ClientOptions. sampleRate := clientOptions.TracesSampleRate + s.sampleRate = sampleRate if sampleRate < 0.0 || sampleRate > 1.0 { Logger.Printf("Dropping transaction: TracesSamplerRate out of range [0.0, 1.0]: %f", sampleRate) return SampledFalse @@ -388,6 +400,12 @@ func (s *Span) toEvent() *Event { finished = append(finished, child) } + // Create and attach a DynamicSamplingContext to the transaction. + // If the DynamicSamplingContext is not frozen at this point, we can assume being head of trace. + if !s.dynamicSamplingContext.IsFrozen() { + s.dynamicSamplingContext = DynamicSamplingContextFromTransaction(s) + } + return &Event{ Type: transactionType, Transaction: hub.Scope().Transaction(), @@ -400,7 +418,8 @@ func (s *Span) toEvent() *Event { StartTime: s.StartTime, Spans: finished, sdkMetaData: SDKMetaData{ - dsc: s.dynamicSamplingContext, + dsc: s.dynamicSamplingContext, + transactionSource: s.Source, }, } } @@ -459,6 +478,18 @@ var ( zeroSpanID SpanID ) +// Contains information about how the name of the transaction was determined. +type TransactionSource string + +const ( + SourceCustom TransactionSource = "custom" + SourceURL TransactionSource = "url" + SourceRoute TransactionSource = "route" + SourceView TransactionSource = "view" + SourceComponent TransactionSource = "component" + SourceTask TransactionSource = "task" +) + // SpanStatus is the status of a span. type SpanStatus uint8 @@ -639,13 +670,20 @@ func OpName(name string) SpanOption { } } +// TransctionSource sets the source of the transaction name. +func TransctionSource(source TransactionSource) SpanOption { + return func(s *Span) { + s.Source = source + } +} + // ContinueFromRequest returns a span option that updates the span to continue // an existing trace. If it cannot detect an existing trace in the request, the // span will be left unchanged. // // ContinueFromRequest is an alias for: // -// ContinueFromHeaders(r.Header.Get("sentry-trace"), r.Header.Get("baggage")) +// ContinueFromHeaders(r.Header.Get("sentry-trace"), r.Header.Get("baggage")). func ContinueFromRequest(r *http.Request) SpanOption { return ContinueFromHeaders(r.Header.Get("sentry-trace"), r.Header.Get("baggage")) } From 327b39a9a249ad7ea9140fe21b64df7e434547b2 Mon Sep 17 00:00:00 2001 From: Michi Hoffmann Date: Mon, 5 Dec 2022 11:11:51 +0100 Subject: [PATCH 15/16] [Dynamic Sampling] TransactionInfo (#508) Co-authored-by: Abhijeet Prasad --- dynamic_sampling_context_test.go | 2 +- interfaces.go | 22 ++++++++++++++-------- tracing.go | 9 +++++++-- tracing_test.go | 9 +++++++++ transport.go | 9 +++++---- 5 files changed, 36 insertions(+), 15 deletions(-) diff --git a/dynamic_sampling_context_test.go b/dynamic_sampling_context_test.go index ca8bec70..6957f838 100644 --- a/dynamic_sampling_context_test.go +++ b/dynamic_sampling_context_test.go @@ -93,7 +93,7 @@ func TestDynamicSamplingContextFromTransaction(t *testing.T) { Dsn: "http://public@example.com/sentry/1", Release: "1.0.0", }) - txn := StartTransaction(ctx, "name") + txn := StartTransaction(ctx, "name", TransctionSource(SourceURL)) txn.TraceID = TraceIDFromHex("d49d9bf66f13450b81f65bc51cf49c03") return txn }(), diff --git a/interfaces.go b/interfaces.go index 5adf1251..0d20ba39 100644 --- a/interfaces.go +++ b/interfaces.go @@ -218,8 +218,12 @@ type Exception struct { // SDKMetaData is a struct to stash data which is needed at some point in the SDK's event processing pipeline // but which shouldn't get send to Sentry. type SDKMetaData struct { - dsc DynamicSamplingContext - transactionSource TransactionSource + dsc DynamicSamplingContext +} + +// Contains information about how the name of the transaction was determined. +type TransactionInfo struct { + Source TransactionSource `json:"source,omitempty"` } // EventID is a hexadecimal string representing a unique uuid4 for an Event. @@ -255,9 +259,10 @@ type Event struct { // The fields below are only relevant for transactions. - Type string `json:"type,omitempty"` - StartTime time.Time `json:"start_timestamp"` - Spans []*Span `json:"spans,omitempty"` + Type string `json:"type,omitempty"` + StartTime time.Time `json:"start_timestamp"` + Spans []*Span `json:"spans,omitempty"` + TransactionInfo *TransactionInfo `json:"transaction_info,omitempty"` // The fields below are not part of the final JSON payload. @@ -303,9 +308,10 @@ func (e *Event) defaultMarshalJSON() ([]byte, error) { // be sent for transactions. They shadow the respective fields in Event // and are meant to remain nil, triggering the omitempty behavior. - Type json.RawMessage `json:"type,omitempty"` - StartTime json.RawMessage `json:"start_timestamp,omitempty"` - Spans json.RawMessage `json:"spans,omitempty"` + Type json.RawMessage `json:"type,omitempty"` + StartTime json.RawMessage `json:"start_timestamp,omitempty"` + Spans json.RawMessage `json:"spans,omitempty"` + TransactionInfo json.RawMessage `json:"transaction_info,omitempty"` } x := errorEvent{event: (*event)(e)} diff --git a/tracing.go b/tracing.go index 33b4e5aa..158e9409 100644 --- a/tracing.go +++ b/tracing.go @@ -91,6 +91,9 @@ func StartSpan(ctx context.Context, operation string, options ...SpanOption) *Sp if hasParent { span.TraceID = parent.TraceID } else { + // Only set the Source if this is a transaction + span.Source = SourceCustom + // Implementation note: // // While math/rand is ~2x faster than crypto/rand (exact @@ -417,9 +420,11 @@ func (s *Span) toEvent() *Event { Timestamp: s.EndTime, StartTime: s.StartTime, Spans: finished, + TransactionInfo: &TransactionInfo{ + Source: s.Source, + }, sdkMetaData: SDKMetaData{ - dsc: s.dynamicSamplingContext, - transactionSource: s.Source, + dsc: s.dynamicSamplingContext, }, } } diff --git a/tracing_test.go b/tracing_test.go index dc98277a..8902f75d 100644 --- a/tracing_test.go +++ b/tracing_test.go @@ -148,6 +148,9 @@ func TestStartSpan(t *testing.T) { Extra: span.Data, Timestamp: endTime, StartTime: startTime, + TransactionInfo: &TransactionInfo{ + Source: span.Source, + }, } opts := cmp.Options{ cmpopts.IgnoreFields(Event{}, @@ -209,6 +212,9 @@ func TestStartChild(t *testing.T) { Sampled: SampledTrue, }, }, + TransactionInfo: &TransactionInfo{ + Source: span.Source, + }, } opts := cmp.Options{ cmpopts.IgnoreFields(Event{}, @@ -285,6 +291,9 @@ func TestStartTransaction(t *testing.T) { Extra: transaction.Data, Timestamp: endTime, StartTime: startTime, + TransactionInfo: &TransactionInfo{ + Source: transaction.Source, + }, } opts := cmp.Options{ cmpopts.IgnoreFields(Event{}, diff --git a/transport.go b/transport.go index 6661f6a2..3722217e 100644 --- a/transport.go +++ b/transport.go @@ -98,11 +98,12 @@ func transactionEnvelopeFromBody(event *Event, dsn *Dsn, sentAt time.Time, body var b bytes.Buffer enc := json.NewEncoder(&b) - dsc := event.sdkMetaData.dsc + // Construct the trace envelope header var trace = map[string]string{} - - for k, v := range dsc.Entries { - trace[k] = v + if dsc := event.sdkMetaData.dsc; dsc.HasEntries() { + for k, v := range dsc.Entries { + trace[k] = v + } } // Envelope header From 5e7d1f2c68e139b485927662e86047315af5f597 Mon Sep 17 00:00:00 2001 From: Michi Hoffmann Date: Mon, 5 Dec 2022 11:12:06 +0100 Subject: [PATCH 16/16] [Dynamic Sampling] String() (#507) --- dynamic_sampling_context.go | 17 ++++++++++++++++- dynamic_sampling_context_test.go | 20 ++++++++++++++++++++ tracing.go | 6 +----- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/dynamic_sampling_context.go b/dynamic_sampling_context.go index 1cdab28f..3e54839b 100644 --- a/dynamic_sampling_context.go +++ b/dynamic_sampling_context.go @@ -90,6 +90,21 @@ func (d DynamicSamplingContext) IsFrozen() bool { } func (d DynamicSamplingContext) String() string { - // TODO implement me + 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 "" } diff --git a/dynamic_sampling_context_test.go b/dynamic_sampling_context_test.go index 6957f838..6cca5659 100644 --- a/dynamic_sampling_context_test.go +++ b/dynamic_sampling_context_test.go @@ -1,6 +1,7 @@ package sentry import ( + "strings" "testing" ) @@ -128,3 +129,22 @@ func TestHasEntries(t *testing.T) { } 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) +} diff --git a/tracing.go b/tracing.go index 158e9409..2d305e1b 100644 --- a/tracing.go +++ b/tracing.go @@ -232,11 +232,7 @@ func (s *Span) ToSentryTrace() string { } func (s *Span) ToBaggage() string { - if s.dynamicSamplingContext.HasEntries() { - return s.dynamicSamplingContext.String() - } - - return "" + return s.dynamicSamplingContext.String() } // sentryTracePattern matches either