Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 85 additions & 64 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,6 @@ type EventProcessor func(event *Event, hint *EventHint) *Event
// needing the otel dependency on the root package.
type externalContextTraceResolver func(ctx context.Context) (traceID TraceID, spanID SpanID, ok bool)

// EventModifier is the interface that wraps the ApplyToEvent method.
//
// ApplyToEvent changes an event based on external data and/or
// an event hint.
type EventModifier interface {
ApplyToEvent(event *Event, hint *EventHint, client *Client) *Event
}

var globalEventProcessors []EventProcessor

// AddGlobalEventProcessor adds processor to the global list of event
Expand Down Expand Up @@ -645,20 +637,32 @@ func (client *Client) GetDataCollection() DataCollection {
return *cloneDataCollection(client.options.DataCollection)
}

// captureOptions carries the per-capture values that take part in scope
// merging precedence. It stays private; the public CaptureOption API is
// parsed into this representation by the context capture PR.
type captureOptions struct {
// hint carries metadata to event processors and before-send hooks.
hint *EventHint
// level is an explicit level from the capture call.
level Level
}

// CaptureMessage captures an arbitrary message.
func (client *Client) CaptureMessage(message string, hint *EventHint, scope EventModifier) *EventID {
event := client.EventFromMessage(message, LevelInfo)
return client.CaptureEvent(event, hint, scope)
func (client *Client) CaptureMessage(message string, hint *EventHint, scope *Scope) *EventID {
event := client.eventFromMessage(message)
event.Level = LevelInfo
return client.processEvent(event, scope, captureOptions{hint: hint})
}

// CaptureException captures an error.
func (client *Client) CaptureException(exception error, hint *EventHint, scope EventModifier) *EventID {
event := client.EventFromException(exception, LevelError)
return client.CaptureEvent(event, hint, scope)
func (client *Client) CaptureException(exception error, hint *EventHint, scope *Scope) *EventID {
event := client.eventFromException(exception)
event.Level = LevelError
return client.processEvent(event, scope, captureOptions{hint: hint})
Comment thread
cursor[bot] marked this conversation as resolved.
}

// CaptureCheckIn captures a check in.
func (client *Client) CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig, scope EventModifier) *EventID {
func (client *Client) CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig, scope *Scope) *EventID {
event := client.EventFromCheckIn(checkIn, monitorConfig)
if event != nil && event.CheckIn != nil {
client.CaptureEvent(event, nil, scope)
Expand All @@ -672,18 +676,16 @@ func (client *Client) CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorCon
// The event must already be assembled. Typically, code would instead use
// the utility methods like CaptureException. The return value is the
// event ID. In case Sentry is disabled or event was dropped, the return value will be nil.
func (client *Client) CaptureEvent(event *Event, hint *EventHint, scope EventModifier) *EventID {
if !client.IsEnabled() {
return nil
}
return client.processEvent(event, hint, scope)
func (client *Client) CaptureEvent(event *Event, hint *EventHint, scope *Scope) *EventID {
return client.processEvent(event, scope, captureOptions{hint: hint})
}

func (client *Client) captureLog(log *Log, _ *Scope) bool {
if !client.IsEnabled() || log == nil {
func (client *Client) captureLog(log *Log, capture signalCaptureContext) bool {
if log == nil {
return false
}

prepareLog(log, client, capture)
if client.options.BeforeSendLog != nil {
approxSize := log.ApproximateSize()
log = client.options.BeforeSendLog(log)
Expand Down Expand Up @@ -713,11 +715,16 @@ func (client *Client) captureLog(log *Log, _ *Scope) bool {
return true
}

func (client *Client) captureMetric(metric *Metric, _ *Scope) bool {
if !client.IsEnabled() || metric == nil {
func (client *Client) recordDiscard(reason report.DiscardReason, category ratelimit.Category, count int64) {
client.reportRecorder.Record(reason, category, count)
}

func (client *Client) captureMetric(metric *Metric, capture signalCaptureContext) bool {
if metric == nil {
return false
}

prepareMetric(metric, client, capture)
if client.options.BeforeSendMetric != nil {
metric = client.options.BeforeSendMetric(metric)
if metric == nil {
Expand Down Expand Up @@ -746,10 +753,7 @@ func (client *Client) captureMetric(metric *Metric, _ *Scope) bool {

// Recover captures a panic.
// Returns EventID if successfully, or nil if there's no error to recover from.
func (client *Client) Recover(err any, hint *EventHint, scope EventModifier) *EventID {
if !client.IsEnabled() {
return nil
}
func (client *Client) Recover(err any, hint *EventHint, scope *Scope) *EventID {
if err == nil {
err = recover()
}
Expand All @@ -768,7 +772,7 @@ func (client *Client) RecoverWithContext(
ctx context.Context,
err any,
hint *EventHint,
scope EventModifier,
scope *Scope,
) *EventID {
if err == nil {
err = recover()
Expand All @@ -777,25 +781,26 @@ func (client *Client) RecoverWithContext(
return nil
}

if ctx != nil {
if hint == nil {
hint = &EventHint{}
}
if hint.Context == nil {
hint.Context = ctx
if ctx != nil && (hint == nil || hint.Context == nil) {
resolved := EventHint{}
if hint != nil {
resolved = *hint
}
resolved.Context = ctx
hint = &resolved
}

var event *Event
switch err := err.(type) {
case error:
event = client.EventFromException(err, LevelFatal)
event = client.eventFromException(err)
case string:
event = client.EventFromMessage(err, LevelFatal)
event = client.eventFromMessage(err)
default:
event = client.EventFromMessage(fmt.Sprintf("%#v", err), LevelFatal)
event = client.eventFromMessage(fmt.Sprintf("%#v", err))
}
return client.CaptureEvent(event, hint, scope)
event.Level = LevelFatal
return client.processEvent(event, scope, captureOptions{hint: hint})
}

// Flush waits until the underlying Transport sends any buffered events to the
Expand All @@ -810,9 +815,6 @@ func (client *Client) RecoverWithContext(
// the network synchronously, configure it to use the HTTPSyncTransport in the
// call to Init.
func (client *Client) Flush(timeout time.Duration) bool {
if !client.IsEnabled() {
return true
}
if client.batchLogger != nil || client.batchMeter != nil || client.telemetryProcessor != nil {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
Expand All @@ -834,9 +836,6 @@ func (client *Client) Flush(timeout time.Duration) bool {
// configure the SDK to use HTTPSyncTransport during initialization with Init.

func (client *Client) FlushWithContext(ctx context.Context) bool {
if !client.IsEnabled() {
return true
}
if client.batchLogger != nil {
client.batchLogger.Flush(ctx.Done())
}
Expand All @@ -854,9 +853,6 @@ func (client *Client) FlushWithContext(ctx context.Context) bool {
// Close should be called after Flush and before terminating the program
// otherwise some events may be lost.
func (client *Client) Close() {
if !client.IsEnabled() {
return
}
if client.telemetryProcessor != nil {
client.telemetryProcessor.Close(5 * time.Second)
}
Expand All @@ -871,12 +867,17 @@ func (client *Client) Close() {

// EventFromMessage creates an event from the given message string.
func (client *Client) EventFromMessage(message string, level Level) *Event {
event := client.eventFromMessage(message)
event.Level = level
return event
}

func (client *Client) eventFromMessage(message string) *Event {
if message == "" {
err := usageError{fmt.Errorf("%s called with empty message", callerFunctionName())}
return client.EventFromException(err, level)
return client.eventFromException(err)
}
event := NewEvent()
event.Level = level
event.Message = message

if client.options.AttachStacktrace {
Expand All @@ -892,8 +893,13 @@ func (client *Client) EventFromMessage(message string, level Level) *Event {

// EventFromException creates a new Sentry event from the given `error` instance.
func (client *Client) EventFromException(exception error, level Level) *Event {
event := NewEvent()
event := client.eventFromException(exception)
event.Level = level
return event
}

func (client *Client) eventFromException(exception error) *Event {
event := NewEvent()

err := exception
if err == nil {
Expand Down Expand Up @@ -946,10 +952,14 @@ func (client *Client) GetSDKIdentifier() string {
return client.sdkIdentifier
}

func (client *Client) processEvent(event *Event, hint *EventHint, scope EventModifier) *EventID {
func (client *Client) GetSDKVersion() string {
return client.sdkVersion
}

func (client *Client) processEvent(event *Event, scope *Scope, opts captureOptions) *EventID {
if event == nil {
err := usageError{fmt.Errorf("%s called with nil event", callerFunctionName())}
return client.CaptureException(err, hint, scope)
return client.CaptureException(err, opts.hint, scope)
}

// Transactions are sampled by options.TracesSampleRate or
Expand All @@ -961,11 +971,12 @@ func (client *Client) processEvent(event *Event, hint *EventHint, scope EventMod
return nil
}

if event = client.prepareEvent(event, hint, scope); event == nil {
if event = client.prepareEvent(event, scope, opts); event == nil {
return nil
}

// Apply beforeSend* processors
hint := opts.hint
if hint == nil {
hint = &EventHint{}
}
Expand Down Expand Up @@ -1007,7 +1018,22 @@ func (client *Client) processEvent(event *Event, hint *EventHint, scope EventMod
return &event.EventID
}

func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventModifier) *Event {
// applyScopeChain merges the passed scope and global scope into event, resolves the trace
// context, and runs the scope event processors. It returns nil when a processor drops the event.
func applyScopeChain(event *Event, client *Client, scope *Scope, opts captureOptions) *Event {
client = normalizeClient(client)
state := resolveCaptureState(event, scope)

var ctx context.Context
if opts.hint != nil {
ctx = opts.hint.Context
}
applyTraceToEvent(event, resolveTrace(scope, client, ctx))

return state.applyToEvent(event, opts.hint, client, opts)
}

func (client *Client) prepareEvent(event *Event, scope *Scope, opts captureOptions) *Event {
if event.EventID == "" {
// TODO set EventID when the event is created, same as in other SDKs. It's necessary for profileTransaction.ID.
event.EventID = EventID(uuid())
Expand All @@ -1017,10 +1043,6 @@ func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventMod
event.Timestamp = time.Now()
}

if event.Level == "" {
event.Level = LevelInfo
}

if event.ServerName == "" {
event.ServerName = client.options.ServerName

Expand Down Expand Up @@ -1052,13 +1074,12 @@ func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventMod
}},
}

if scope != nil {
event = scope.ApplyToEvent(event, hint, client)
if event == nil {
return nil
}
event = applyScopeChain(event, client, scope, opts)
if event == nil {
return nil
}

hint := opts.hint
for _, processor := range client.eventProcessors {
id := event.EventID
category := event.toCategory()
Expand Down
16 changes: 9 additions & 7 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func TestNewClientAllowsEmptyDSN(t *testing.T) {
t.Fatalf("expected no error when creating client without a DNS but got %v", err)
}

client.CaptureException(errors.New("custom error"), nil, &MockScope{})
client.CaptureException(errors.New("custom error"), nil, NewScope())
assertEqual(t, transport.lastEvent.Exception[0].Value, "custom error")
}

Expand All @@ -51,8 +51,8 @@ func (e customComplexError) AnswerToLife() string {
return "42"
}

func setupClientTest() (*Client, *MockScope, *MockTransport) {
scope := &MockScope{}
func setupClientTest() (*Client, *Scope, *MockTransport) {
scope := NewScope()
transport := &MockTransport{}
client, _ := NewClient(ClientOptions{
Dsn: "http://whatever@example.com/1337",
Expand Down Expand Up @@ -511,7 +511,9 @@ func TestSampleRateCanDropEvent(t *testing.T) {

func TestApplyToScopeCanDropEvent(t *testing.T) {
client, scope, transport := setupClientTest()
scope.shouldDropEvent = true
scope.AddEventProcessor(func(_ *Event, _ *EventHint) *Event {
return nil
})

client.AddEventProcessor(func(event *Event, _ *EventHint) *Event {
if event == nil {
Expand Down Expand Up @@ -644,7 +646,7 @@ func TestIgnoreErrors(t *testing.T) {

for name, tt := range tests {
t.Run(name, func(t *testing.T) {
scope := &MockScope{}
scope := NewScope()
transport := &MockTransport{}
client, err := NewClient(ClientOptions{
Transport: transport,
Expand Down Expand Up @@ -946,7 +948,7 @@ func BenchmarkProcessEvent(b *testing.B) {
b.Fatal(err)
}
for i := 0; i < b.N; i++ {
c.processEvent(&Event{}, nil, nil)
c.processEvent(&Event{}, nil, captureOptions{})
}
}

Expand Down Expand Up @@ -1105,7 +1107,7 @@ func TestTelemetryEnvelopeCarriesIntegrations(t *testing.T) {
require.NoError(t, err)
t.Cleanup(func() { client.Close() })

client.CaptureMessage("ping", nil, &MockScope{})
client.CaptureMessage("ping", nil, NewScope())
require.True(t, client.Flush(testutils.FlushTimeout()), "flush timed out")

select {
Expand Down
9 changes: 9 additions & 0 deletions dynamic_sampling_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,15 @@ func (d DynamicSamplingContext) String() string {
return baggage.String()
}

func dynamicSamplingContextFromScope(scope *Scope, client *Client) DynamicSamplingContext {
if scope == nil {
return DynamicSamplingContextFromScope(nil, client)
}
scope.mu.RLock()
defer scope.mu.RUnlock()
return DynamicSamplingContextFromScope(scope, client)
}

// DynamicSamplingContextFromScope Constructs a new DynamicSamplingContext using a scope and client. Accessing
// fields on the scope are not thread safe, and this function should only be
// called within scope methods.
Expand Down
Loading
Loading