Skip to content

Commit 2be08f1

Browse files
committed
feat!: introduce context-based capture APIs
Require context.Context across package capture APIs, bind clients and isolation scopes directly through context, and preserve last-event and propagation state on scopes. Keep Hub capture paths as a compatibility bridge while the stack migrates. BREAKING CHANGE: Package capture APIs now require context.Context and CaptureOption values. Direct Client capture methods, RecoverWithContext, CapturePanic, EventFromMessage, EventFromException, EventFromCheckIn, and EventModifier are removed.
1 parent a00abe6 commit 2be08f1

15 files changed

Lines changed: 778 additions & 263 deletions

capture_options.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package sentry
2+
3+
import "context"
4+
5+
type CaptureOption func(*captureOptions)
6+
7+
type captureOptions struct {
8+
hint *EventHint
9+
level Level
10+
defaultLevel Level
11+
}
12+
13+
func WithEventHint(hint *EventHint) CaptureOption {
14+
return func(options *captureOptions) {
15+
options.hint = hint
16+
}
17+
}
18+
19+
func WithLevel(level Level) CaptureOption {
20+
return func(options *captureOptions) {
21+
options.level = level
22+
}
23+
}
24+
25+
func resolveCaptureOptions(ctx context.Context, options ...CaptureOption) captureOptions {
26+
if len(options) == 0 {
27+
return captureOptions{hint: &EventHint{Context: ctx}}
28+
}
29+
return resolveCaptureOptionsWithOptions(ctx, options)
30+
}
31+
32+
func resolveCaptureOptionsWithOptions(ctx context.Context, options []CaptureOption) captureOptions {
33+
var resolved captureOptions
34+
for _, option := range options {
35+
if option != nil {
36+
option(&resolved)
37+
}
38+
}
39+
40+
hint := new(EventHint)
41+
if resolved.hint != nil {
42+
*hint = *resolved.hint
43+
}
44+
if ctx != nil {
45+
hint.Context = ctx
46+
}
47+
resolved.hint = hint
48+
return resolved
49+
}

client.go

Lines changed: 79 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,6 @@ type EventProcessor func(event *Event, hint *EventHint) *Event
102102
// needing the otel dependency on the root package.
103103
type externalContextTraceResolver func(ctx context.Context) (traceID TraceID, spanID SpanID, ok bool)
104104

105-
// EventModifier is the interface that wraps the ApplyToEvent method.
106-
//
107-
// ApplyToEvent changes an event based on external data and/or
108-
// an event hint.
109-
type EventModifier interface {
110-
ApplyToEvent(event *Event, hint *EventHint, client *Client) *Event
111-
}
112-
113105
var globalEventProcessors []EventProcessor
114106

115107
// AddGlobalEventProcessor adds processor to the global list of event
@@ -641,48 +633,68 @@ func (client *Client) GetDataCollection() DataCollection {
641633
return *cloneDataCollection(client.options.DataCollection)
642634
}
643635

644-
// CaptureMessage captures an arbitrary message.
645-
func (client *Client) CaptureMessage(message string, hint *EventHint, scope EventModifier) *EventID {
636+
// CaptureMessage captures an arbitrary message using the scope carried by ctx.
637+
func (client *Client) CaptureMessage(ctx context.Context, message string, options ...CaptureOption) *EventID {
646638
if !client.IsEnabled() {
647639
return nil
648640
}
649-
event := client.EventFromMessage(message, LevelInfo)
650-
return client.CaptureEvent(event, hint, scope)
641+
opts := resolveCaptureOptions(ctx, options...)
642+
opts.defaultLevel = LevelInfo
643+
if message == "" {
644+
err := usageError{fmt.Errorf("%s called with empty message", callerFunctionName())}
645+
return client.capture(ctx, client.eventFromException(err), opts)
646+
}
647+
return client.capture(ctx, client.eventFromMessage(message), opts)
651648
}
652649

653-
// CaptureException captures an error.
654-
func (client *Client) CaptureException(exception error, hint *EventHint, scope EventModifier) *EventID {
650+
// CaptureException captures an error using the scope carried by ctx.
651+
func (client *Client) CaptureException(ctx context.Context, exception error, options ...CaptureOption) *EventID {
655652
if !client.IsEnabled() {
656653
return nil
657654
}
658-
event := client.EventFromException(exception, LevelError)
659-
return client.CaptureEvent(event, hint, scope)
655+
opts := resolveCaptureOptions(ctx, options...)
656+
if opts.hint.OriginalException == nil {
657+
opts.hint.OriginalException = exception
658+
}
659+
opts.defaultLevel = LevelError
660+
if exception == nil {
661+
exception = usageError{fmt.Errorf("%s called with nil error", callerFunctionName())}
662+
}
663+
return client.capture(ctx, client.eventFromException(exception), opts)
660664
}
661665

662-
// CaptureCheckIn captures a check in.
663-
func (client *Client) CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig, scope EventModifier) *EventID {
666+
// CaptureCheckIn captures a check-in using the scope carried by ctx.
667+
func (client *Client) CaptureCheckIn(
668+
ctx context.Context,
669+
checkIn *CheckIn,
670+
monitorConfig *MonitorConfig,
671+
options ...CaptureOption,
672+
) *EventID {
664673
if !client.IsEnabled() {
665674
return nil
666675
}
667-
event := client.EventFromCheckIn(checkIn, monitorConfig)
668-
if event != nil && event.CheckIn != nil {
669-
if client.CaptureEvent(event, nil, scope) != nil {
670-
return &event.CheckIn.ID
671-
}
676+
event := client.eventFromCheckIn(checkIn, monitorConfig)
677+
if event == nil {
678+
return nil
672679
}
673-
return nil
680+
id := event.CheckIn.ID
681+
if client.capture(ctx, event, resolveCaptureOptions(ctx, options...)) == nil {
682+
return nil
683+
}
684+
return &id
674685
}
675686

676-
// CaptureEvent captures an event on the currently active client if any.
677-
//
678-
// The event must already be assembled. Typically, code would instead use
679-
// the utility methods like CaptureException. The return value is the
680-
// event ID. In case Sentry is disabled or event was dropped, the return value will be nil.
681-
func (client *Client) CaptureEvent(event *Event, hint *EventHint, scope EventModifier) *EventID {
687+
// CaptureEvent captures an event using the scope carried by ctx.
688+
func (client *Client) CaptureEvent(ctx context.Context, event *Event, options ...CaptureOption) *EventID {
682689
if !client.IsEnabled() {
683690
return nil
684691
}
685-
return client.processEvent(event, hint, scope)
692+
opts := resolveCaptureOptions(ctx, options...)
693+
if event == nil {
694+
event = client.eventFromException(usageError{fmt.Errorf("%s called with nil event", callerFunctionName())})
695+
opts.defaultLevel = LevelError
696+
}
697+
return client.capture(ctx, event, opts)
686698
}
687699

688700
func (client *Client) captureLog(log *Log, _ *Scope) bool {
@@ -750,58 +762,27 @@ func (client *Client) captureMetric(metric *Metric, _ *Scope) bool {
750762
return true
751763
}
752764

753-
// Recover captures a panic.
754-
// Returns EventID if successfully, or nil if there's no error to recover from.
755-
func (client *Client) Recover(err any, hint *EventHint, scope EventModifier) *EventID {
756-
if err == nil {
757-
err = recover()
758-
}
759-
760-
// Normally we would not pass a nil Context, but RecoverWithContext doesn't
761-
// use the Context for communicating deadline nor cancelation. All it does
762-
// is store the Context in the EventHint and there nil means the Context is
763-
// not available.
764-
// nolint: staticcheck
765-
return client.RecoverWithContext(nil, err, hint, scope)
766-
}
767-
768-
// RecoverWithContext captures a panic and passes relevant context object.
769-
// Returns EventID if successfully, or nil if there's no error to recover from.
770-
func (client *Client) RecoverWithContext(
771-
ctx context.Context,
772-
err any,
773-
hint *EventHint,
774-
scope EventModifier,
775-
) *EventID {
776-
if err == nil {
777-
err = recover()
778-
}
779-
if err == nil {
780-
return nil
781-
}
782-
if !client.IsEnabled() {
765+
func (client *Client) capturePanic(ctx context.Context, recovered any, options ...CaptureOption) *EventID {
766+
if recovered == nil || !client.IsEnabled() {
783767
return nil
784768
}
785769

786-
if ctx != nil {
787-
if hint == nil {
788-
hint = &EventHint{}
789-
}
790-
if hint.Context == nil {
791-
hint.Context = ctx
792-
}
770+
opts := resolveCaptureOptions(ctx, options...)
771+
if opts.hint.RecoveredException == nil {
772+
opts.hint.RecoveredException = recovered
793773
}
794774

795775
var event *Event
796-
switch err := err.(type) {
776+
switch recovered := recovered.(type) {
797777
case error:
798-
event = client.EventFromException(err, LevelFatal)
778+
event = client.eventFromException(recovered)
799779
case string:
800-
event = client.EventFromMessage(err, LevelFatal)
780+
event = client.eventFromMessage(recovered)
801781
default:
802-
event = client.EventFromMessage(fmt.Sprintf("%#v", err), LevelFatal)
782+
event = client.eventFromMessage(fmt.Sprintf("%#v", recovered))
803783
}
804-
return client.CaptureEvent(event, hint, scope)
784+
opts.defaultLevel = LevelFatal
785+
return client.capture(ctx, event, opts)
805786
}
806787

807788
// Flush waits until the underlying Transport sends any buffered events to the
@@ -876,14 +857,8 @@ func (client *Client) Close() {
876857
client.Transport.Close()
877858
}
878859

879-
// EventFromMessage creates an event from the given message string.
880-
func (client *Client) EventFromMessage(message string, level Level) *Event {
881-
if message == "" {
882-
err := usageError{fmt.Errorf("%s called with empty message", callerFunctionName())}
883-
return client.EventFromException(err, level)
884-
}
860+
func (client *Client) eventFromMessage(message string) *Event {
885861
event := NewEvent()
886-
event.Level = level
887862
event.Message = message
888863

889864
if client.options.AttachStacktrace {
@@ -897,23 +872,14 @@ func (client *Client) EventFromMessage(message string, level Level) *Event {
897872
return event
898873
}
899874

900-
// EventFromException creates a new Sentry event from the given `error` instance.
901-
func (client *Client) EventFromException(exception error, level Level) *Event {
875+
func (client *Client) eventFromException(exception error) *Event {
902876
event := NewEvent()
903-
event.Level = level
904-
905-
err := exception
906-
if err == nil {
907-
err = usageError{fmt.Errorf("%s called with nil error", callerFunctionName())}
908-
}
909-
910-
event.SetException(err, client.options.MaxErrorDepth)
877+
event.SetException(exception, client.options.MaxErrorDepth)
911878

912879
return event
913880
}
914881

915-
// EventFromCheckIn creates a new Sentry event from the given `check_in` instance.
916-
func (client *Client) EventFromCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *Event {
882+
func (client *Client) eventFromCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *Event {
917883
if checkIn == nil {
918884
return nil
919885
}
@@ -956,12 +922,7 @@ func (client *Client) GetSDKIdentifier() string {
956922
return client.sdkIdentifier
957923
}
958924

959-
func (client *Client) processEvent(event *Event, hint *EventHint, scope EventModifier) *EventID {
960-
if event == nil {
961-
err := usageError{fmt.Errorf("%s called with nil event", callerFunctionName())}
962-
return client.CaptureException(err, hint, scope)
963-
}
964-
925+
func (client *Client) capture(ctx context.Context, event *Event, opts captureOptions) *EventID {
965926
// Transactions are sampled by options.TracesSampleRate or
966927
// options.TracesSampler when they are started. Other events
967928
// (errors, messages) are sampled here. Does not apply to check-ins.
@@ -971,14 +932,13 @@ func (client *Client) processEvent(event *Event, hint *EventHint, scope EventMod
971932
return nil
972933
}
973934

974-
if event = client.prepareEvent(event, hint, scope); event == nil {
935+
scope := scopeFromContextOrGlobal(ctx)
936+
if event = client.prepareEvent(event, scope, opts); event == nil {
975937
return nil
976938
}
977939

978940
// Apply beforeSend* processors
979-
if hint == nil {
980-
hint = &EventHint{}
981-
}
941+
hint := opts.hint
982942
switch event.Type {
983943
case transactionType:
984944
if client.options.BeforeSendTransaction != nil {
@@ -1009,15 +969,19 @@ func (client *Client) processEvent(event *Event, hint *EventHint, scope EventMod
1009969
if client.telemetryProcessor != nil {
1010970
if !client.telemetryProcessor.Add(event) {
1011971
debuglog.Println("Event dropped: telemetry buffer full or unavailable")
972+
return nil
1012973
}
1013974
} else {
1014975
client.Transport.SendEvent(event)
1015976
}
1016977

978+
if event.Type != transactionType && event.Type != checkInType {
979+
scope.setLastEventID(event.EventID)
980+
}
1017981
return &event.EventID
1018982
}
1019983

1020-
func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventModifier) *Event {
984+
func (client *Client) prepareEvent(event *Event, scope *Scope, opts captureOptions) *Event {
1021985
if event.EventID == "" {
1022986
// TODO set EventID when the event is created, same as in other SDKs. It's necessary for profileTransaction.ID.
1023987
event.EventID = EventID(uuid())
@@ -1027,10 +991,6 @@ func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventMod
1027991
event.Timestamp = time.Now()
1028992
}
1029993

1030-
if event.Level == "" {
1031-
event.Level = LevelInfo
1032-
}
1033-
1034994
if event.ServerName == "" {
1035995
event.ServerName = client.options.ServerName
1036996

@@ -1063,17 +1023,26 @@ func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventMod
10631023
}
10641024

10651025
if scope != nil {
1066-
event = scope.ApplyToEvent(event, hint, client)
1026+
event = scope.ApplyToEvent(event, opts.hint, client)
10671027
if event == nil {
10681028
return nil
10691029
}
10701030
}
1031+
if event.Level == "" {
1032+
event.Level = opts.defaultLevel
1033+
}
1034+
if event.Level == "" {
1035+
event.Level = LevelInfo
1036+
}
1037+
if opts.level != "" {
1038+
event.Level = opts.level
1039+
}
10711040

10721041
for _, processor := range client.eventProcessors {
10731042
id := event.EventID
10741043
category := event.toCategory()
10751044
spanCountBefore := event.GetSpanCount()
1076-
event = processor(event, hint)
1045+
event = processor(event, opts.hint)
10771046
if event == nil {
10781047
debuglog.Printf("Event dropped by one of the Client EventProcessors: %s\n", id)
10791048
client.reportRecorder.RecordOne(report.ReasonEventProcessor, category)
@@ -1094,7 +1063,7 @@ func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventMod
10941063
id := event.EventID
10951064
category := event.toCategory()
10961065
spanCountBefore := event.GetSpanCount()
1097-
event = processor(event, hint)
1066+
event = processor(event, opts.hint)
10981067
if event == nil {
10991068
debuglog.Printf("Event dropped by one of the Global EventProcessors: %s\n", id)
11001069
client.reportRecorder.RecordOne(report.ReasonEventProcessor, category)

0 commit comments

Comments
 (0)