From 18b7a4a0a6dd0c54779eb9034f84787760a07422 Mon Sep 17 00:00:00 2001 From: Giannis Gkiortzis <58184179+giortzisg@users.noreply.github.com> Date: Fri, 17 Jul 2026 12:13:00 +0200 Subject: [PATCH] feat: add noop client implementation --- _examples/multiclient/main.go | 2 +- _examples/with_extra/main.go | 2 +- client.go | 107 +++++++------ client_api.go | 101 ++++++++++++ client_api_test.go | 149 ++++++++++++++++++ client_reports_test.go | 4 +- client_test.go | 34 ++-- data_collection_filter_test.go | 2 +- data_collection_test.go | 6 +- dynamic_sampling_context.go | 23 +-- dynamic_sampling_context_test.go | 30 ++-- echo/sentryecho.go | 4 +- fasthttp/sentryfasthttp.go | 4 +- fiber/sentryfiber.go | 4 +- fiberv3/sentryfiber.go | 4 +- gin/sentrygin.go | 4 +- grpc/client.go | 4 +- grpc/server.go | 6 +- grpc/server_internal_test.go | 12 +- http/sentryhttp.go | 4 +- httpclient/sentryhttpclient.go | 24 +-- hub.go | 70 +++----- hub_test.go | 24 ++- integrations.go | 19 +-- integrations_test.go | 4 +- interfaces.go | 2 +- interfaces_test.go | 4 +- internal/sentrytest/fixture.go | 4 +- .../telemetry/attachments_regression_test.go | 2 +- iris/sentryiris.go | 4 +- log.go | 19 +-- log_batch_processor.go | 2 +- log_race_test.go | 10 +- log_test.go | 25 ++- logrus/logrusentry.go | 4 +- logrus/logrusentry_test.go | 2 +- metric_batch_processor.go | 2 +- metrics.go | 23 ++- metrics_test.go | 21 ++- mocks.go | 2 +- negroni/sentrynegroni.go | 4 +- otel/linking_integration.go | 2 +- recover_external_test.go | 6 +- scope.go | 51 +++--- scope_test.go | 28 ++-- span_recorder.go | 8 +- span_recorder_test.go | 2 +- tracing.go | 36 ++--- tracing_test.go | 4 +- 49 files changed, 550 insertions(+), 364 deletions(-) create mode 100644 client_api.go create mode 100644 client_api_test.go diff --git a/_examples/multiclient/main.go b/_examples/multiclient/main.go index 55b00f6ab..6d1cecf77 100644 --- a/_examples/multiclient/main.go +++ b/_examples/multiclient/main.go @@ -13,7 +13,7 @@ func (pi *pickleIntegration) Name() string { return "PickleIntegration" } -func (pi *pickleIntegration) SetupOnce(client *sentry.Client) { +func (pi *pickleIntegration) SetupOnce(client sentry.Client) { client.AddEventProcessor(pi.processor) } diff --git a/_examples/with_extra/main.go b/_examples/with_extra/main.go index 4dee8f816..73c925e33 100644 --- a/_examples/with_extra/main.go +++ b/_examples/with_extra/main.go @@ -49,7 +49,7 @@ func (ee ExtractExtra) Name() string { return "ExtractExtra" } -func (ee ExtractExtra) SetupOnce(client *sentry.Client) { +func (ee ExtractExtra) SetupOnce(client sentry.Client) { client.AddEventProcessor(func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { if ex, ok := hint.OriginalException.(CustomComplexError); ok { for key, val := range ex.GimmeMoreData() { diff --git a/client.go b/client.go index 7886bd0eb..828fad85c 100644 --- a/client.go +++ b/client.go @@ -107,7 +107,7 @@ type externalContextTraceResolver func(ctx context.Context) (traceID TraceID, sp // ApplyToEvent changes an event based on external data and/or // an event hint. type EventModifier interface { - ApplyToEvent(event *Event, hint *EventHint, client *Client) *Event + ApplyToEvent(event *Event, hint *EventHint, client Client) *Event } var globalEventProcessors []EventProcessor @@ -125,7 +125,7 @@ func AddGlobalEventProcessor(processor EventProcessor) { // Integration allows for registering a functions that modify or discard captured events. type Integration interface { Name() string - SetupOnce(client *Client) + SetupOnce(client Client) } // ClientOptions that configures a SDK Client. @@ -299,9 +299,8 @@ type ClientOptions struct { DisableTelemetryBuffer bool } -// Client is the underlying processor that is used by the main API and Hub -// instances. It must be created with NewClient. -type Client struct { +// defaultClient is the standard Client implementation created by NewClient. +type defaultClient struct { mu sync.RWMutex options ClientOptions dsn *protocol.Dsn @@ -309,7 +308,6 @@ type Client struct { integrations []Integration externalTraceResolver externalContextTraceResolver sdkIdentifier string - sdkVersion string // Transport is read-only. Replacing the transport of an existing client is // not supported, create a new client instead. Transport Transport @@ -320,14 +318,12 @@ type Client struct { reportProvider report.ClientReportProvider } -// NewClient creates and returns an instance of Client configured using -// ClientOptions. -// -// Most users will not create clients directly. Instead, initialize the SDK with -// Init and use the package-level functions (for simple programs that run on a -// single goroutine) or hub methods (for concurrent programs, for example web -// servers). -func NewClient(options ClientOptions) (*Client, error) { +// NewClient creates and returns a Client configured using ClientOptions. +func NewClient(options ClientOptions) (Client, error) { + return newClient(options) +} + +func newClient(options ClientOptions) (*defaultClient, error) { // The default error event sample rate for all SDKs is 1.0 (send all). // // In Go, the zero value (default) for float64 is 0.0, which means that @@ -417,11 +413,10 @@ func NewClient(options ClientOptions) (*Client, error) { } } - client := Client{ + client := defaultClient{ options: options, dsn: dsn, sdkIdentifier: sdkIdentifier, - sdkVersion: SDKVersion, reportRecorder: report.NoopRecorder(), reportProvider: report.NoopProvider(), } @@ -459,7 +454,7 @@ func NewClient(options ClientOptions) (*Client, error) { return &client, nil } -func (client *Client) setupTransport() { +func (client *defaultClient) setupTransport() { opts := client.options transport := opts.Transport @@ -491,7 +486,7 @@ func (client *Client) setupTransport() { client.Transport = transport } -func (client *Client) sdkInfo() *protocol.SdkInfo { +func (client *defaultClient) sdkInfo() *protocol.SdkInfo { return &protocol.SdkInfo{ Name: client.GetSDKIdentifier(), Version: SDKVersion, @@ -503,7 +498,7 @@ func (client *Client) sdkInfo() *protocol.SdkInfo { } } -func (client *Client) setupTelemetryProcessor() { +func (client *defaultClient) setupTelemetryProcessor() { transport := httpInternal.NewAsyncTransport(httpInternal.TransportOptions{ Dsn: client.options.Dsn, HTTPClient: client.options.HTTPClient, @@ -528,7 +523,7 @@ func (client *Client) setupTelemetryProcessor() { client.telemetryProcessor = telemetry.NewProcessor(buffers, transport, client.dsn, client.sdkInfo, client.reportRecorder) } -func (client *Client) setupIntegrations() { +func (client *defaultClient) setupIntegrations() { integrations := []Integration{ new(environmentIntegration), new(modulesIntegration), @@ -556,6 +551,9 @@ func (client *Client) setupIntegrations() { }) } +// IsEnabled reports whether the client processes telemetry. +func (client *defaultClient) IsEnabled() bool { return true } + // AddEventProcessor adds an event processor to the client. It must not be // called from concurrent goroutines. Most users will prefer to use // ClientOptions.BeforeSend or Scope.AddEventProcessor instead. @@ -563,7 +561,7 @@ func (client *Client) setupIntegrations() { // Note that typical programs have only a single client created by Init and the // client is shared among multiple hubs, one per goroutine, such that adding an // event processor to the client affects all hubs that share the client. -func (client *Client) AddEventProcessor(processor EventProcessor) { +func (client *defaultClient) AddEventProcessor(processor EventProcessor) { client.eventProcessors = append(client.eventProcessors, processor) } @@ -571,14 +569,14 @@ func (client *Client) AddEventProcessor(processor EventProcessor) { // from external context implementations. // // This is intended for integrations such as OpenTelemetry. -func (client *Client) SetExternalContextTraceResolver(resolver func(ctx context.Context) (TraceID, SpanID, bool)) { +func (client *defaultClient) SetExternalContextTraceResolver(resolver func(ctx context.Context) (TraceID, SpanID, bool)) { client.mu.Lock() defer client.mu.Unlock() client.externalTraceResolver = resolver } -func (client *Client) externalTraceContextFromContext(ctx context.Context) (TraceID, SpanID, bool) { +func (client *defaultClient) externalTraceContextFromContext(ctx context.Context) (TraceID, SpanID, bool) { if ctx == nil { return TraceID{}, SpanID{}, false } @@ -595,36 +593,39 @@ func (client *Client) externalTraceContextFromContext(ctx context.Context) (Trac } // Options return ClientOptions for the current Client. -func (client *Client) Options() ClientOptions { - // Note: internally, consider using `client.options` instead of `client.Options()` to avoid copying the object each time. +func (client *defaultClient) Options() ClientOptions { opts := client.options opts.DataCollection = cloneDataCollection(client.options.DataCollection) return opts } +func (client *defaultClient) clientOptions() *ClientOptions { + return &client.options +} + // GetDataCollection returns a copy of the resolved data collection // configuration used by the client. -func (client *Client) GetDataCollection() DataCollection { - if client == nil || client.options.DataCollection == nil { +func (client *defaultClient) GetDataCollection() DataCollection { + if client.options.DataCollection == nil { return DataCollection{} } return *cloneDataCollection(client.options.DataCollection) } // CaptureMessage captures an arbitrary message. -func (client *Client) CaptureMessage(message string, hint *EventHint, scope EventModifier) *EventID { +func (client *defaultClient) CaptureMessage(message string, hint *EventHint, scope EventModifier) *EventID { event := client.EventFromMessage(message, LevelInfo) return client.CaptureEvent(event, hint, scope) } // CaptureException captures an error. -func (client *Client) CaptureException(exception error, hint *EventHint, scope EventModifier) *EventID { +func (client *defaultClient) CaptureException(exception error, hint *EventHint, scope EventModifier) *EventID { event := client.EventFromException(exception, LevelError) return client.CaptureEvent(event, hint, scope) } // CaptureCheckIn captures a check in. -func (client *Client) CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig, scope EventModifier) *EventID { +func (client *defaultClient) CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig, scope EventModifier) *EventID { event := client.EventFromCheckIn(checkIn, monitorConfig) if event != nil && event.CheckIn != nil { client.CaptureEvent(event, nil, scope) @@ -638,11 +639,11 @@ 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 { +func (client *defaultClient) CaptureEvent(event *Event, hint *EventHint, scope EventModifier) *EventID { return client.processEvent(event, hint, scope) } -func (client *Client) captureLog(log *Log, _ *Scope) bool { +func (client *defaultClient) captureLog(log *Log, _ *Scope) bool { if log == nil { return false } @@ -676,7 +677,15 @@ func (client *Client) captureLog(log *Log, _ *Scope) bool { return true } -func (client *Client) captureMetric(metric *Metric, _ *Scope) bool { +func (client *defaultClient) getDsn() *protocol.Dsn { + return client.dsn +} + +func (client *defaultClient) recordDiscard(reason report.DiscardReason, category ratelimit.Category, count int64) { + client.reportRecorder.Record(reason, category, count) +} + +func (client *defaultClient) captureMetric(metric *Metric, _ *Scope) bool { if metric == nil { return false } @@ -709,7 +718,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 { +func (client *defaultClient) Recover(err any, hint *EventHint, scope EventModifier) *EventID { if err == nil { err = recover() } @@ -724,7 +733,7 @@ func (client *Client) Recover(err any, hint *EventHint, scope EventModifier) *Ev // RecoverWithContext captures a panic and passes relevant context object. // Returns EventID if successfully, or nil if there's no error to recover from. -func (client *Client) RecoverWithContext( +func (client *defaultClient) RecoverWithContext( ctx context.Context, err any, hint *EventHint, @@ -769,7 +778,7 @@ func (client *Client) RecoverWithContext( // CaptureException or CaptureMessage. Instead, to have the SDK send events over // the network synchronously, configure it to use the HTTPSyncTransport in the // call to Init. -func (client *Client) Flush(timeout time.Duration) bool { +func (client *defaultClient) Flush(timeout time.Duration) bool { if client.batchLogger != nil || client.batchMeter != nil || client.telemetryProcessor != nil { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() @@ -790,7 +799,7 @@ func (client *Client) Flush(timeout time.Duration) bool { // CaptureException, or CaptureMessage. To send events synchronously over the network, // configure the SDK to use HTTPSyncTransport during initialization with Init. -func (client *Client) FlushWithContext(ctx context.Context) bool { +func (client *defaultClient) FlushWithContext(ctx context.Context) bool { if client.batchLogger != nil { client.batchLogger.Flush(ctx.Done()) } @@ -807,7 +816,7 @@ 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() { +func (client *defaultClient) Close() { if client.telemetryProcessor != nil { client.telemetryProcessor.Close(5 * time.Second) } @@ -821,7 +830,7 @@ func (client *Client) Close() { } // EventFromMessage creates an event from the given message string. -func (client *Client) EventFromMessage(message string, level Level) *Event { +func (client *defaultClient) EventFromMessage(message string, level Level) *Event { if message == "" { err := usageError{fmt.Errorf("%s called with empty message", callerFunctionName())} return client.EventFromException(err, level) @@ -842,7 +851,7 @@ 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 { +func (client *defaultClient) EventFromException(exception error, level Level) *Event { event := NewEvent() event.Level = level @@ -857,7 +866,7 @@ func (client *Client) EventFromException(exception error, level Level) *Event { } // EventFromCheckIn creates a new Sentry event from the given `check_in` instance. -func (client *Client) EventFromCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *Event { +func (client *defaultClient) EventFromCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *Event { if checkIn == nil { return nil } @@ -883,21 +892,25 @@ func (client *Client) EventFromCheckIn(checkIn *CheckIn, monitorConfig *MonitorC return event } -func (client *Client) SetSDKIdentifier(identifier string) { +func (client *defaultClient) SetSDKIdentifier(identifier string) { client.mu.Lock() defer client.mu.Unlock() client.sdkIdentifier = identifier } -func (client *Client) GetSDKIdentifier() string { +func (client *defaultClient) GetSDKIdentifier() string { client.mu.RLock() defer client.mu.RUnlock() return client.sdkIdentifier } -func (client *Client) processEvent(event *Event, hint *EventHint, scope EventModifier) *EventID { +func (client *defaultClient) GetSDKVersion() string { + return SDKVersion +} + +func (client *defaultClient) processEvent(event *Event, hint *EventHint, scope EventModifier) *EventID { if event == nil { err := usageError{fmt.Errorf("%s called with nil event", callerFunctionName())} return client.CaptureException(err, hint, scope) @@ -958,7 +971,7 @@ func (client *Client) processEvent(event *Event, hint *EventHint, scope EventMod return &event.EventID } -func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventModifier) *Event { +func (client *defaultClient) prepareEvent(event *Event, hint *EventHint, scope EventModifier) *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()) @@ -1055,7 +1068,7 @@ func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventMod return event } -func (client *Client) listIntegrations() []string { +func (client *defaultClient) listIntegrations() []string { integrations := make([]string, len(client.integrations)) for i, integration := range client.integrations { integrations[i] = integration.Name() @@ -1063,7 +1076,7 @@ func (client *Client) listIntegrations() []string { return integrations } -func (client *Client) integrationAlreadyInstalled(name string) bool { +func (client *defaultClient) integrationAlreadyInstalled(name string) bool { for _, integration := range client.integrations { if integration.Name() == name { return true diff --git a/client_api.go b/client_api.go new file mode 100644 index 000000000..aa866d83c --- /dev/null +++ b/client_api.go @@ -0,0 +1,101 @@ +package sentry + +import ( + "context" + "reflect" + "time" + + "github.com/getsentry/sentry-go/internal/protocol" + "github.com/getsentry/sentry-go/internal/ratelimit" + "github.com/getsentry/sentry-go/report" +) + +// Client captures and delivers telemetry to Sentry. +type Client interface { + IsEnabled() bool + AddEventProcessor(EventProcessor) + SetExternalContextTraceResolver(func(context.Context) (TraceID, SpanID, bool)) + Options() ClientOptions + clientOptions() *ClientOptions + GetDataCollection() DataCollection + CaptureMessage(string, *EventHint, EventModifier) *EventID + CaptureException(error, *EventHint, EventModifier) *EventID + CaptureCheckIn(*CheckIn, *MonitorConfig, EventModifier) *EventID + CaptureEvent(*Event, *EventHint, EventModifier) *EventID + Recover(any, *EventHint, EventModifier) *EventID + RecoverWithContext(context.Context, any, *EventHint, EventModifier) *EventID + Flush(time.Duration) bool + FlushWithContext(context.Context) bool + Close() + EventFromMessage(string, Level) *Event + EventFromException(error, Level) *Event + EventFromCheckIn(*CheckIn, *MonitorConfig) *Event + SetSDKIdentifier(string) + GetSDKIdentifier() string + GetSDKVersion() string + externalTraceContextFromContext(context.Context) (TraceID, SpanID, bool) + captureLog(*Log, *Scope) bool + captureMetric(*Metric, *Scope) bool + recordDiscard(report.DiscardReason, ratelimit.Category, int64) + getDsn() *protocol.Dsn +} + +type noopClient struct{} + +// noopClientOptions is shared by every noopClient via clientOptions. It must +// never be mutated: callers of clientOptions must treat the returned options +// as read-only. +var noopClientOptions = ClientOptions{ + DisableLogs: true, + DisableMetrics: true, +} + +// NewNoopClient returns a Client that discards all telemetry. +func NewNoopClient() Client { return noopClient{} } + +func normalizeClient(value Client) Client { + if value == nil { + return noopClient{} + } + // Guard against typed-nil Client implementations (e.g. a nil + // *defaultClient or a nil pointer to a user-defined implementation), + // which would otherwise panic on first use. + if v := reflect.ValueOf(value); v.Kind() == reflect.Pointer && v.IsNil() { + return noopClient{} + } + return value +} + +func (noopClient) IsEnabled() bool { return false } +func (noopClient) AddEventProcessor(EventProcessor) {} +func (noopClient) SetExternalContextTraceResolver(func(context.Context) (TraceID, SpanID, bool)) { +} +func (noopClient) Options() ClientOptions { return noopClientOptions } +func (noopClient) clientOptions() *ClientOptions { return &noopClientOptions } +func (noopClient) GetDataCollection() DataCollection { return DataCollection{} } +func (noopClient) CaptureMessage(string, *EventHint, EventModifier) *EventID { return nil } +func (noopClient) CaptureException(error, *EventHint, EventModifier) *EventID { return nil } +func (noopClient) CaptureCheckIn(*CheckIn, *MonitorConfig, EventModifier) *EventID { + return nil +} +func (noopClient) CaptureEvent(*Event, *EventHint, EventModifier) *EventID { return nil } +func (noopClient) Recover(any, *EventHint, EventModifier) *EventID { return nil } +func (noopClient) RecoverWithContext(context.Context, any, *EventHint, EventModifier) *EventID { + return nil +} +func (noopClient) Flush(time.Duration) bool { return true } +func (noopClient) FlushWithContext(context.Context) bool { return true } +func (noopClient) Close() {} +func (noopClient) EventFromMessage(string, Level) *Event { return nil } +func (noopClient) EventFromException(error, Level) *Event { return nil } +func (noopClient) EventFromCheckIn(*CheckIn, *MonitorConfig) *Event { return nil } +func (noopClient) SetSDKIdentifier(string) {} +func (noopClient) GetSDKIdentifier() string { return sdkIdentifier } +func (noopClient) GetSDKVersion() string { return SDKVersion } +func (noopClient) externalTraceContextFromContext(context.Context) (TraceID, SpanID, bool) { + return TraceID{}, SpanID{}, false +} +func (noopClient) captureLog(*Log, *Scope) bool { return false } +func (noopClient) captureMetric(*Metric, *Scope) bool { return false } +func (noopClient) recordDiscard(report.DiscardReason, ratelimit.Category, int64) {} +func (noopClient) getDsn() *protocol.Dsn { return nil } diff --git a/client_api_test.go b/client_api_test.go new file mode 100644 index 000000000..529f4f072 --- /dev/null +++ b/client_api_test.go @@ -0,0 +1,149 @@ +package sentry + +import ( + "context" + "errors" + "reflect" + "testing" + "time" + + "github.com/getsentry/sentry-go/internal/ratelimit" + "github.com/getsentry/sentry-go/report" +) + +func TestNoopClient(t *testing.T) { + t.Parallel() + + client := NewNoopClient() + if client.IsEnabled() { + t.Fatal("no-op client must be disabled") + } + + options := client.Options() + if !options.DisableLogs { + t.Error("no-op client must disable logs") + } + if !options.DisableMetrics { + t.Error("no-op client must disable metrics") + } + + client.AddEventProcessor(func(event *Event, _ *EventHint) *Event { return event }) + client.SetExternalContextTraceResolver(func(context.Context) (TraceID, SpanID, bool) { + return TraceID{}, SpanID{}, true + }) + client.SetSDKIdentifier("custom") + + if got := client.CaptureMessage("message", nil, nil); got != nil { + t.Errorf("CaptureMessage returned %v, want nil", got) + } + if got := client.CaptureException(errors.New("boom"), nil, nil); got != nil { + t.Errorf("CaptureException returned %v, want nil", got) + } + if got := client.CaptureCheckIn(&CheckIn{}, nil, nil); got != nil { + t.Errorf("CaptureCheckIn returned %v, want nil", got) + } + if got := client.CaptureEvent(NewEvent(), nil, nil); got != nil { + t.Errorf("CaptureEvent returned %v, want nil", got) + } + if got := client.Recover(errors.New("boom"), nil, nil); got != nil { + t.Errorf("Recover returned %v, want nil", got) + } + if got := client.RecoverWithContext(t.Context(), errors.New("boom"), nil, nil); got != nil { + t.Errorf("RecoverWithContext returned %v, want nil", got) + } + + if got := client.EventFromMessage("message", LevelInfo); got != nil { + t.Errorf("EventFromMessage returned %v, want nil", got) + } + if got := client.EventFromException(errors.New("boom"), LevelError); got != nil { + t.Errorf("EventFromException returned %v, want nil", got) + } + if got := client.EventFromCheckIn(&CheckIn{}, nil); got != nil { + t.Errorf("EventFromCheckIn returned %v, want nil", got) + } + + if !client.Flush(time.Millisecond) { + t.Error("no-op Flush must report success") + } + if !client.FlushWithContext(t.Context()) { + t.Error("no-op FlushWithContext must report success") + } + client.Close() + + if got := client.GetDataCollection(); !reflect.DeepEqual(got, DataCollection{}) { + t.Errorf("GetDataCollection returned %#v, want empty configuration", got) + } + if got := client.GetSDKIdentifier(); got != sdkIdentifier { + t.Errorf("GetSDKIdentifier returned %q, want %q", got, sdkIdentifier) + } + if got := client.GetSDKVersion(); got != SDKVersion { + t.Errorf("GetSDKVersion returned %q, want %q", got, SDKVersion) + } + if dsn := client.getDsn(); dsn != nil { + t.Errorf("getDsn returned %#v, want nil", dsn) + } + if traceID, spanID, ok := client.externalTraceContextFromContext(t.Context()); ok || traceID != (TraceID{}) || spanID != (SpanID{}) { + t.Errorf("external trace resolver returned (%v, %v, %v), want zero values", traceID, spanID, ok) + } + if client.captureLog(&Log{}, NewScope()) { + t.Error("no-op client must discard logs") + } + if client.captureMetric(&Metric{}, NewScope()) { + t.Error("no-op client must discard metrics") + } + client.recordDiscard(report.ReasonSampleRate, ratelimit.CategoryTransaction, 1) + + if event := NewScope().ApplyToEvent(NewEvent(), nil, nil); event == nil { + t.Error("scope application with a nil client must normalize to the no-op client") + } + if got := DynamicSamplingContextFromScope(NewScope(), nil); got.HasEntries() || got.IsFrozen() { + t.Errorf("DynamicSamplingContextFromScope returned %#v with nil client, want empty context", got) + } +} + +func TestNewClientReturnsEnabledClient(t *testing.T) { + t.Parallel() + + client, err := NewClient(ClientOptions{Transport: &MockTransport{}}) + if err != nil { + t.Fatal(err) + } + t.Cleanup(client.Close) + if !client.IsEnabled() { + t.Error("NewClient returned a disabled client") + } +} + +// stubClient is a user-defined Client implementation used to exercise +// normalizeClient with typed-nil pointers of types other than *defaultClient. +type stubClient struct{ Client } + +func TestNormalizeClient(t *testing.T) { + t.Parallel() + + tests := map[string]Client{ + "nil interface": nil, + "nil default client": (*defaultClient)(nil), + "nil custom client": (*stubClient)(nil), + "no-op client": noopClient{}, + } + for name, input := range tests { + t.Run(name, func(t *testing.T) { + if got := normalizeClient(input); got == nil || got.IsEnabled() { + t.Fatalf("normalizeClient(%T) = %#v, want disabled non-nil client", input, got) + } + }) + } +} + +func TestHubWithoutClientFlushSucceeds(t *testing.T) { + t.Parallel() + + hub := NewHub(nil, NewScope()) + if !hub.Flush(time.Millisecond) { + t.Error("Flush on a hub without a client must report success") + } + if !hub.FlushWithContext(t.Context()) { + t.Error("FlushWithContext on a hub without a client must report success") + } +} diff --git a/client_reports_test.go b/client_reports_test.go index 050c1b525..32343c395 100644 --- a/client_reports_test.go +++ b/client_reports_test.go @@ -36,7 +36,7 @@ func TestClientReports_Integration(t *testing.T) { dsn := strings.Replace(srv.URL, "//", "//test@", 1) + "/1" hub := CurrentHub().Clone() - c, err := NewClient(ClientOptions{ + c, err := newClient(ClientOptions{ Dsn: dsn, DisableClientReports: false, SampleRate: 1.0, @@ -54,7 +54,7 @@ func TestClientReports_Integration(t *testing.T) { defer hub.Flush(testutils.FlushTimeout()) // second client with disabled reports shouldn't affect the first - _, _ = NewClient(ClientOptions{ + _, _ = newClient(ClientOptions{ Dsn: testDsn, DisableClientReports: true, }) diff --git a/client_test.go b/client_test.go index d83c15a2c..f00a6ebe3 100644 --- a/client_test.go +++ b/client_test.go @@ -28,7 +28,7 @@ import ( func TestNewClientAllowsEmptyDSN(t *testing.T) { transport := &MockTransport{} - client, err := NewClient(ClientOptions{ + client, err := newClient(ClientOptions{ Transport: transport, }) if err != nil { @@ -51,10 +51,10 @@ func (e customComplexError) AnswerToLife() string { return "42" } -func setupClientTest() (*Client, *MockScope, *MockTransport) { +func setupClientTest() (*defaultClient, *MockScope, *MockTransport) { scope := &MockScope{} transport := &MockTransport{} - client, _ := NewClient(ClientOptions{ + client, _ := newClient(ClientOptions{ Dsn: "http://whatever@example.com/1337", Transport: transport, // keep default buffers enabled @@ -646,7 +646,7 @@ func TestIgnoreErrors(t *testing.T) { t.Run(name, func(t *testing.T) { scope := &MockScope{} transport := &MockTransport{} - client, err := NewClient(ClientOptions{ + client, err := newClient(ClientOptions{ Transport: transport, IgnoreErrors: tt.ignoreErrors, }) @@ -906,7 +906,7 @@ func TestSampleRate(t *testing.T) { } } func TestClient_ParseOrgID(t *testing.T) { - c, err := NewClient(ClientOptions{ + c, err := newClient(ClientOptions{ Dsn: "https://example@o1.ingest.us.sentry.io/1337", }) if err != nil { @@ -916,7 +916,7 @@ func TestClient_ParseOrgID(t *testing.T) { } func TestClient_ParseOrgIDInvalid(t *testing.T) { - c, err := NewClient(ClientOptions{ + c, err := newClient(ClientOptions{ // org id is MaxUint64 + 1, should be considered empty Dsn: "https://example@o18446744073709551616.ingest.us.sentry.io/1337", }) @@ -927,7 +927,7 @@ func TestClient_ParseOrgIDInvalid(t *testing.T) { } func TestClientOptions_OrgIDShouldOverrideParsed(t *testing.T) { - c, err := NewClient(ClientOptions{ + c, err := newClient(ClientOptions{ Dsn: "https://example@o1.ingest.us.sentry.io/1337", OrgID: 2, }) @@ -938,7 +938,7 @@ func TestClientOptions_OrgIDShouldOverrideParsed(t *testing.T) { } func BenchmarkProcessEvent(b *testing.B) { - c, err := NewClient(ClientOptions{ + c, err := newClient(ClientOptions{ SampleRate: 0.25, Transport: &MockTransport{}, }) @@ -1039,7 +1039,7 @@ func TestCustomMaxSpansProperty(t *testing.T) { client.options.MaxSpans = 2000 assertEqual(t, client.Options().MaxSpans, 2000) - properClient, _ := NewClient(ClientOptions{ + properClient, _ := newClient(ClientOptions{ MaxSpans: 3000, Transport: &MockTransport{}, }) @@ -1056,7 +1056,7 @@ func TestSDKIdentifier(t *testing.T) { } func TestClientSetsUpTransport(t *testing.T) { - client, _ := NewClient(ClientOptions{ + client, _ := newClient(ClientOptions{ Dsn: testDsn, HTTPClient: &http.Client{ Transport: &http.Transport{ @@ -1072,8 +1072,8 @@ func TestClientSetsUpTransport(t *testing.T) { type namedIntegration struct{ name string } -func (n *namedIntegration) Name() string { return n.name } -func (n *namedIntegration) SetupOnce(_ *Client) {} +func (n *namedIntegration) Name() string { return n.name } +func (n *namedIntegration) SetupOnce(_ Client) {} func TestTelemetryEnvelopeCarriesIntegrations(t *testing.T) { var ( @@ -1096,7 +1096,7 @@ func TestTelemetryEnvelopeCarriesIntegrations(t *testing.T) { defer srv.Close() dsn := strings.Replace(srv.URL, "//", "//pubkey@", 1) + "/1" - client, err := NewClient(ClientOptions{ + client, err := newClient(ClientOptions{ Dsn: dsn, Integrations: func(defaults []Integration) []Integration { return append(defaults, &namedIntegration{name: "CustomRegressionIntegration"}) @@ -1137,7 +1137,7 @@ func TestClient_SetupTelemetryBuffer_NoDSN(t *testing.T) { debuglog.SetOutput(&buf) defer debuglog.SetOutput(&bytes.Buffer{}) - client, err := NewClient(ClientOptions{}) + client, err := newClient(ClientOptions{}) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -1149,7 +1149,7 @@ func TestClient_SetupTelemetryBuffer_NoDSN(t *testing.T) { } type multiClientEnv struct { - client1, client2 *Client + client1, client2 *defaultClient transport1, transport2 *MockTransport hub1, hub2 *Hub ctx1, ctx2 context.Context @@ -1158,9 +1158,9 @@ type multiClientEnv struct { func setupMultiClientEnv(t *testing.T) *multiClientEnv { t.Helper() - mkClient := func(dsn string) (*Client, *MockTransport) { + mkClient := func(dsn string) (*defaultClient, *MockTransport) { tr := &MockTransport{} - c, err := NewClient(ClientOptions{ + c, err := newClient(ClientOptions{ Dsn: dsn, Transport: tr, Integrations: func(_ []Integration) []Integration { diff --git a/data_collection_filter_test.go b/data_collection_filter_test.go index 8f7502454..3eaf7c2ca 100644 --- a/data_collection_filter_test.go +++ b/data_collection_filter_test.go @@ -485,7 +485,7 @@ func newClientDataCollection(t *testing.T, dc *DataCollection) DataCollection { if dc == nil { dc = &DataCollection{} } - client, err := NewClient(ClientOptions{ + client, err := newClient(ClientOptions{ Dsn: "https://key@sentry.io/1", DataCollection: dc, }) diff --git a/data_collection_test.go b/data_collection_test.go index d41e7490a..1c3688ec6 100644 --- a/data_collection_test.go +++ b/data_collection_test.go @@ -91,7 +91,7 @@ func TestNewClientDataCollection(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - client, err := NewClient(tt.options) + client, err := newClient(tt.options) if err != nil { t.Fatal(err) } @@ -111,7 +111,7 @@ func TestNewClientDataCollectionSnapshotting(t *testing.T) { Cookies: &KeyValueCollectionBehavior{Mode: CollectionAllowList, Terms: []string{"session_id"}}, HTTPBodies: []BodyType{BodyIncomingRequest}, } - client, err := NewClient(ClientOptions{ + client, err := newClient(ClientOptions{ Dsn: "https://key@sentry.io/1", DataCollection: input, }) @@ -148,7 +148,7 @@ func TestNewClientDataCollectionSnapshotting(t *testing.T) { func TestNewClientLegacyDataCollectionSensitiveTerms(t *testing.T) { t.Parallel() - client, err := NewClient(ClientOptions{Dsn: "https://key@sentry.io/1"}) + client, err := newClient(ClientOptions{Dsn: "https://key@sentry.io/1"}) if err != nil { t.Fatal(err) } diff --git a/dynamic_sampling_context.go b/dynamic_sampling_context.go index a257f6f5d..fe0d5c46a 100644 --- a/dynamic_sampling_context.go +++ b/dynamic_sampling_context.go @@ -43,7 +43,7 @@ func DynamicSamplingContextFromTransaction(span *Span) DynamicSamplingContext { scope := hub.Scope() client := hub.Client() - if client == nil || scope == nil { + if !client.IsEnabled() || scope == nil { return DynamicSamplingContext{ Entries: map[string]string{}, Frozen: false, @@ -59,7 +59,7 @@ func DynamicSamplingContextFromTransaction(span *Span) DynamicSamplingContext { entries["sample_rate"] = strconv.FormatFloat(sampleRate, 'f', -1, 64) } - if dsn := client.dsn; dsn != nil { + if dsn := client.getDsn(); dsn != nil { if publicKey := dsn.GetPublicKey(); publicKey != "" { entries["public_key"] = publicKey } @@ -67,10 +67,11 @@ func DynamicSamplingContextFromTransaction(span *Span) DynamicSamplingContext { entries["org_id"] = strconv.FormatUint(orgID, 10) } } - if release := client.options.Release; release != "" { + options := client.clientOptions() + if release := options.Release; release != "" { entries["release"] = release } - if environment := client.options.Environment; environment != "" { + if environment := options.Environment; environment != "" { entries["environment"] = environment } @@ -119,10 +120,11 @@ func (d DynamicSamplingContext) String() string { // 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. -func DynamicSamplingContextFromScope(scope *Scope, client *Client) DynamicSamplingContext { +func DynamicSamplingContextFromScope(scope *Scope, client Client) DynamicSamplingContext { entries := map[string]string{} + client = normalizeClient(client) - if client == nil || scope == nil { + if scope == nil || !client.IsEnabled() { return DynamicSamplingContext{ Entries: entries, Frozen: false, @@ -134,11 +136,11 @@ func DynamicSamplingContextFromScope(scope *Scope, client *Client) DynamicSampli if traceID := propagationContext.TraceID.String(); traceID != "" { entries["trace_id"] = traceID } - if sampleRate := client.options.TracesSampleRate; sampleRate != 0 { + if sampleRate := client.clientOptions().TracesSampleRate; sampleRate != 0 { entries["sample_rate"] = strconv.FormatFloat(sampleRate, 'f', -1, 64) } - if dsn := client.dsn; dsn != nil { + if dsn := client.getDsn(); dsn != nil { if publicKey := dsn.GetPublicKey(); publicKey != "" { entries["public_key"] = publicKey } @@ -146,10 +148,11 @@ func DynamicSamplingContextFromScope(scope *Scope, client *Client) DynamicSampli entries["org_id"] = strconv.FormatUint(orgID, 10) } } - if release := client.options.Release; release != "" { + options := client.clientOptions() + if release := options.Release; release != "" { entries["release"] = release } - if environment := client.options.Environment; environment != "" { + if environment := options.Environment; environment != "" { entries["environment"] = environment } diff --git a/dynamic_sampling_context_test.go b/dynamic_sampling_context_test.go index e0df45828..5f889e3a7 100644 --- a/dynamic_sampling_context_test.go +++ b/dynamic_sampling_context_test.go @@ -4,7 +4,6 @@ import ( "context" "testing" - "github.com/getsentry/sentry-go/internal/protocol" "github.com/getsentry/sentry-go/internal/testutils" ) @@ -182,7 +181,7 @@ func TestString(t *testing.T) { func TestDynamicSamplingContextFromScope(t *testing.T) { tests := map[string]struct { scope *Scope - client *Client + client Client expected DynamicSamplingContext }{ "Valid input": { @@ -192,16 +191,19 @@ func TestDynamicSamplingContextFromScope(t *testing.T) { SpanID: SpanIDFromHex("a9f442f9330b4e09"), }, }, - client: func() *Client { - dsn, _ := protocol.NewDsn("http://public@example.com/sentry/1") - return &Client{ - options: ClientOptions{ - Dsn: dsn.String(), - Release: "1.0.0", - Environment: "production", - }, - dsn: dsn, + client: func() *defaultClient { + client, err := newClient(ClientOptions{ + Dsn: "http://public@example.com/sentry/1", + Release: "1.0.0", + Environment: "production", + Transport: &MockTransport{}, + DisableLogs: true, + DisableMetrics: true, + }) + if err != nil { + panic(err) } + return client }(), expected: DynamicSamplingContext{ Entries: map[string]string{ @@ -213,14 +215,14 @@ func TestDynamicSamplingContextFromScope(t *testing.T) { Frozen: true, }, }, - "Nil client": { + "No-op client": { scope: &Scope{ propagationContext: PropagationContext{ TraceID: TraceIDFromHex("d49d9bf66f13450b81f65bc51cf49c03"), SpanID: SpanIDFromHex("a9f442f9330b4e09"), }, }, - client: nil, + client: noopClient{}, expected: DynamicSamplingContext{ Entries: map[string]string{}, Frozen: false, @@ -228,7 +230,7 @@ func TestDynamicSamplingContextFromScope(t *testing.T) { }, "Nil scope": { scope: nil, - client: &Client{}, + client: &defaultClient{}, expected: DynamicSamplingContext{ Entries: map[string]string{}, Frozen: false, diff --git a/echo/sentryecho.go b/echo/sentryecho.go index 918ef9dac..84f6adc35 100644 --- a/echo/sentryecho.go +++ b/echo/sentryecho.go @@ -64,9 +64,7 @@ func (h *handler) handle(next echo.HandlerFunc) echo.HandlerFunc { hub = sentry.CurrentHub().Clone() } - if client := hub.Client(); client != nil { - client.SetSDKIdentifier(sdkIdentifier) - } + hub.Client().SetSDKIdentifier(sdkIdentifier) r := ctx.Request() diff --git a/fasthttp/sentryfasthttp.go b/fasthttp/sentryfasthttp.go index cdf98f5c3..da84ad970 100644 --- a/fasthttp/sentryfasthttp.go +++ b/fasthttp/sentryfasthttp.go @@ -64,9 +64,7 @@ func (h *Handler) Handle(handler fasthttp.RequestHandler) fasthttp.RequestHandle hub = sentry.CurrentHub().Clone() } - if client := hub.Client(); client != nil { - client.SetSDKIdentifier(sdkIdentifier) - } + hub.Client().SetSDKIdentifier(sdkIdentifier) r := convert(ctx) diff --git a/fiber/sentryfiber.go b/fiber/sentryfiber.go index 34fd6b17c..fbce623e0 100644 --- a/fiber/sentryfiber.go +++ b/fiber/sentryfiber.go @@ -63,9 +63,7 @@ func (h *handler) handle(ctx *fiber.Ctx) error { hub = sentry.CurrentHub().Clone() } - if client := hub.Client(); client != nil { - client.SetSDKIdentifier(sdkIdentifier) - } + hub.Client().SetSDKIdentifier(sdkIdentifier) r := convert(ctx) diff --git a/fiberv3/sentryfiber.go b/fiberv3/sentryfiber.go index a2f4ceeaa..944eea37a 100644 --- a/fiberv3/sentryfiber.go +++ b/fiberv3/sentryfiber.go @@ -63,9 +63,7 @@ func (h *handler) handle(ctx fiber.Ctx) error { hub = sentry.CurrentHub().Clone() } - if client := hub.Client(); client != nil { - client.SetSDKIdentifier(sdkIdentifier) - } + hub.Client().SetSDKIdentifier(sdkIdentifier) r := convert(ctx) diff --git a/gin/sentrygin.go b/gin/sentrygin.go index bfc551c66..3b8d88cec 100644 --- a/gin/sentrygin.go +++ b/gin/sentrygin.go @@ -63,9 +63,7 @@ func (h *handler) handle(c *gin.Context) { hub = sentry.CurrentHub().Clone() } - if client := hub.Client(); client != nil { - client.SetSDKIdentifier(sdkIdentifier) - } + hub.Client().SetSDKIdentifier(sdkIdentifier) transactionName := c.Request.URL.Path transactionSource := sentry.SourceURL diff --git a/grpc/client.go b/grpc/client.go index 576bc00d3..547e874c1 100644 --- a/grpc/client.go +++ b/grpc/client.go @@ -26,9 +26,7 @@ func hubFromClientContext(ctx context.Context) context.Context { ctx = sentry.SetHubOnContext(ctx, hub) } - if client := hub.Client(); client != nil { - client.SetSDKIdentifier(sdkIdentifier) - } + hub.Client().SetSDKIdentifier(sdkIdentifier) return ctx } diff --git a/grpc/server.go b/grpc/server.go index 19c6addf4..862c385b0 100644 --- a/grpc/server.go +++ b/grpc/server.go @@ -62,9 +62,7 @@ func hubFromServerContext(ctx context.Context) *sentry.Hub { hub = sentry.CurrentHub().Clone() } - if client := hub.Client(); client != nil { - client.SetSDKIdentifier(sdkIdentifier) - } + hub.Client().SetSDKIdentifier(sdkIdentifier) return hub } @@ -185,7 +183,7 @@ func filterMetadataCookies(dc sentry.DataCollection, key string, values []string return cookies, cookies != "" } -func metadataToContext(client *sentry.Client, md metadata.MD) map[string]any { +func metadataToContext(client sentry.Client, md metadata.MD) map[string]any { if len(md) == 0 { return nil } diff --git a/grpc/server_internal_test.go b/grpc/server_internal_test.go index 7030b99af..6e6225f0d 100644 --- a/grpc/server_internal_test.go +++ b/grpc/server_internal_test.go @@ -12,13 +12,13 @@ import ( func TestMetadataToContext(t *testing.T) { tests := []struct { name string - client *sentry.Client + client sentry.Client md metadata.MD want map[string]any }{ { name: "default snapshot filters values and keeps keys", - client: func() *sentry.Client { + client: func() sentry.Client { client, err := sentry.NewClient(sentry.ClientOptions{Dsn: "https://key@sentry.io/1"}) if err != nil { t.Fatal(err) @@ -36,7 +36,7 @@ func TestMetadataToContext(t *testing.T) { }, { name: "explicit data collection filters values and keeps keys", - client: func() *sentry.Client { + client: func() sentry.Client { client, err := sentry.NewClient(sentry.ClientOptions{ Dsn: "https://key@sentry.io/1", DataCollection: &sentry.DataCollection{ @@ -61,7 +61,7 @@ func TestMetadataToContext(t *testing.T) { }, { name: "cookie metadata is filtered by cookie name", - client: func() *sentry.Client { + client: func() sentry.Client { client, err := sentry.NewClient(sentry.ClientOptions{ Dsn: "https://key@sentry.io/1", DataCollection: &sentry.DataCollection{}, @@ -84,7 +84,7 @@ func TestMetadataToContext(t *testing.T) { }, { name: "set-cookie metadata preserves attributes per cookie", - client: func() *sentry.Client { + client: func() sentry.Client { client, err := sentry.NewClient(sentry.ClientOptions{ Dsn: "https://key@sentry.io/1", DataCollection: &sentry.DataCollection{}, @@ -106,7 +106,7 @@ func TestMetadataToContext(t *testing.T) { }, { name: "cookie metadata can be disabled separately", - client: func() *sentry.Client { + client: func() sentry.Client { client, err := sentry.NewClient(sentry.ClientOptions{ Dsn: "https://key@sentry.io/1", DataCollection: &sentry.DataCollection{ diff --git a/http/sentryhttp.go b/http/sentryhttp.go index eb6648145..cc510a5e5 100644 --- a/http/sentryhttp.go +++ b/http/sentryhttp.go @@ -91,9 +91,7 @@ func (h *Handler) handle(handler http.Handler) http.HandlerFunc { ctx = sentry.SetHubOnContext(ctx, hub) } - if client := hub.Client(); client != nil { - client.SetSDKIdentifier(sdkIdentifier) - } + hub.Client().SetSDKIdentifier(sdkIdentifier) options := []sentry.SpanOption{ sentry.ContinueTrace(hub, r.Header.Get(sentry.SentryTraceHeader), r.Header.Get(sentry.SentryBaggageHeader)), diff --git a/httpclient/sentryhttpclient.go b/httpclient/sentryhttpclient.go index 198632c61..041a1f5da 100644 --- a/httpclient/sentryhttpclient.go +++ b/httpclient/sentryhttpclient.go @@ -47,16 +47,11 @@ func NewSentryRoundTripper(originalRoundTripper http.RoundTripper, opts ...Sentr // Configure trace propagation targets var tracePropagationTargets []string var propagateTraceparent bool - if hub := sentry.CurrentHub(); hub != nil { - client := hub.Client() - if client != nil { - clientOptions := client.Options() - if clientOptions.TracePropagationTargets != nil { - tracePropagationTargets = clientOptions.TracePropagationTargets - } - propagateTraceparent = clientOptions.PropagateTraceparent - } + clientOptions := sentry.CurrentHub().Client().Options() + if clientOptions.TracePropagationTargets != nil { + tracePropagationTargets = clientOptions.TracePropagationTargets } + propagateTraceparent = clientOptions.PropagateTraceparent t := &SentryRoundTripper{ originalRoundTripper: originalRoundTripper, @@ -83,16 +78,9 @@ type SentryRoundTripper struct { func dataCollectionFromRequest(request *http.Request) sentry.DataCollection { if hub := sentry.GetHubFromContext(request.Context()); hub != nil { - if client := hub.Client(); client != nil { - return client.GetDataCollection() - } - } - if hub := sentry.CurrentHub(); hub != nil { - if client := hub.Client(); client != nil { - return client.GetDataCollection() - } + return hub.Client().GetDataCollection() } - return sentry.DataCollection{} + return sentry.CurrentHub().Client().GetDataCollection() } func (s *SentryRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) { diff --git a/hub.go b/hub.go index 15420a647..bd376b651 100644 --- a/hub.go +++ b/hub.go @@ -20,8 +20,8 @@ const ( RequestContextKey = contextKey(2) ) -// currentHub is the initial Hub with no Client bound and an empty Scope. -var currentHub = NewHub(nil, NewScope()) +// currentHub is the initial Hub with a no-op Client and an empty Scope. +var currentHub = NewHub(noopClient{}, NewScope()) // Hub is the central object that manages scopes and clients. // @@ -42,29 +42,30 @@ type Hub struct { type layer struct { // mu protects concurrent reads and writes to client. mu sync.RWMutex - client *Client + client Client // scope is read-only, not protected by mu. scope *Scope } // Client returns the layer's client. Safe for concurrent use. -func (l *layer) Client() *Client { +func (l *layer) Client() Client { l.mu.RLock() defer l.mu.RUnlock() return l.client } // SetClient sets the layer's client. Safe for concurrent use. -func (l *layer) SetClient(c *Client) { +func (l *layer) SetClient(client Client) { l.mu.Lock() defer l.mu.Unlock() - l.client = c + l.client = normalizeClient(client) } type stack []*layer // NewHub returns an instance of a Hub with provided Client and Scope bound. -func NewHub(client *Client, scope *Scope) *Hub { +func NewHub(client Client, scope *Scope) *Hub { + client = normalizeClient(client) hub := Hub{ stack: &stack{{ client: client, @@ -126,8 +127,8 @@ func (hub *Hub) Scope() *Scope { return top.scope } -// Client returns top-level Client of the current Hub or nil if no Client is bound. -func (hub *Hub) Client() *Client { +// Client returns the top-level Client of the current Hub. +func (hub *Hub) Client() Client { top := hub.stackTop() return top.Client() } @@ -175,9 +176,8 @@ func (hub *Hub) PopScope() { } // BindClient binds a new Client for the current Hub. -func (hub *Hub) BindClient(client *Client) { - top := hub.stackTop() - top.SetClient(client) +func (hub *Hub) BindClient(client Client) { + hub.stackTop().SetClient(client) } // WithScope runs f in an isolated temporary scope. @@ -218,7 +218,7 @@ func (hub *Hub) CaptureEvent(event *Event) *EventID { // CaptureEventWithHint is like CaptureEvent but additionally accepts an EventHint. func (hub *Hub) CaptureEventWithHint(event *Event, hint *EventHint) *EventID { client, scope := hub.Client(), hub.Scope() - if client == nil || scope == nil { + if scope == nil { return nil } eventID := client.CaptureEvent(event, hint, scope) @@ -236,7 +236,7 @@ func (hub *Hub) CaptureEventWithHint(event *Event, hint *EventHint) *EventID { // Returns EventID if successfully, or nil if there's no Scope or Client available. func (hub *Hub) CaptureMessage(message string) *EventID { client, scope := hub.Client(), hub.Scope() - if client == nil || scope == nil { + if scope == nil { return nil } eventID := client.CaptureMessage(message, nil, scope) @@ -254,7 +254,7 @@ func (hub *Hub) CaptureMessage(message string) *EventID { // Returns EventID if successfully, or nil if there's no Scope or Client available. func (hub *Hub) CaptureException(exception error) *EventID { client, scope := hub.Client(), hub.Scope() - if client == nil || scope == nil { + if scope == nil { return nil } eventID := client.CaptureException(exception, &EventHint{OriginalException: exception}, scope) @@ -271,12 +271,7 @@ func (hub *Hub) CaptureException(exception error) *EventID { // passing it a top-level Scope. // Returns CheckInID if the check-in was captured successfully, or nil otherwise. func (hub *Hub) CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *EventID { - client, scope := hub.Client(), hub.Scope() - if client == nil { - return nil - } - - return client.CaptureCheckIn(checkIn, monitorConfig, scope) + return hub.Client().CaptureCheckIn(checkIn, monitorConfig, hub.Scope()) } // AddBreadcrumb records a new breadcrumb. @@ -285,14 +280,9 @@ func (hub *Hub) CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) * // configuration on the client. func (hub *Hub) AddBreadcrumb(breadcrumb *Breadcrumb, hint *BreadcrumbHint) { client := hub.Client() + options := client.clientOptions() - // If there's no client, just store it on the scope straight away - if client == nil { - hub.Scope().AddBreadcrumb(breadcrumb, defaultMaxBreadcrumbs) - return - } - - limit := client.options.MaxBreadcrumbs + limit := options.MaxBreadcrumbs switch { case limit < 0: return @@ -300,11 +290,11 @@ func (hub *Hub) AddBreadcrumb(breadcrumb *Breadcrumb, hint *BreadcrumbHint) { limit = defaultMaxBreadcrumbs } - if client.options.BeforeBreadcrumb != nil { + if options.BeforeBreadcrumb != nil { if hint == nil { hint = &BreadcrumbHint{} } - if breadcrumb = client.options.BeforeBreadcrumb(breadcrumb, hint); breadcrumb == nil { + if breadcrumb = options.BeforeBreadcrumb(breadcrumb, hint); breadcrumb == nil { debuglog.Println("breadcrumb dropped due to BeforeBreadcrumb callback.") return } @@ -321,7 +311,7 @@ func (hub *Hub) Recover(err interface{}) *EventID { err = recover() } client, scope := hub.Client(), hub.Scope() - if client == nil || scope == nil { + if scope == nil { return nil } return client.Recover(err, &EventHint{RecoveredException: err}, scope) @@ -335,7 +325,7 @@ func (hub *Hub) RecoverWithContext(ctx context.Context, err interface{}) *EventI err = recover() } client, scope := hub.Client(), hub.Scope() - if client == nil || scope == nil { + if scope == nil { return nil } return client.RecoverWithContext(ctx, err, &EventHint{RecoveredException: err}, scope) @@ -353,13 +343,7 @@ func (hub *Hub) RecoverWithContext(ctx context.Context, err interface{}) *EventI // the network synchronously, configure it to use the HTTPSyncTransport in the // call to Init. func (hub *Hub) Flush(timeout time.Duration) bool { - client := hub.Client() - - if client == nil { - return false - } - - return client.Flush(timeout) + return hub.Client().Flush(timeout) } // FlushWithContext waits until the underlying Transport sends any buffered events @@ -375,13 +359,7 @@ func (hub *Hub) Flush(timeout time.Duration) bool { // configure the SDK to use HTTPSyncTransport during initialization with Init. func (hub *Hub) FlushWithContext(ctx context.Context) bool { - client := hub.Client() - - if client == nil { - return false - } - - return client.FlushWithContext(ctx) + return hub.Client().FlushWithContext(ctx) } // GetTraceparent returns the current Sentry traceparent string, to be used as a HTTP header value diff --git a/hub_test.go b/hub_test.go index 45e552bf4..4a660a866 100644 --- a/hub_test.go +++ b/hub_test.go @@ -17,8 +17,8 @@ import ( const testDsn = "http://whatever@example.com/1337" -func setupHubTest() (*Hub, *Client, *Scope) { - client, _ := NewClient(ClientOptions{Dsn: testDsn, Transport: &MockTransport{}}) +func setupHubTest() (*Hub, *defaultClient, *Scope) { + client, _ := newClient(ClientOptions{Dsn: testDsn, Transport: &MockTransport{}}) scope := NewScope() hub := NewHub(client, scope) return hub, client, scope @@ -105,7 +105,7 @@ func TestPopScopeCannotLeaveStackEmpty(t *testing.T) { func TestBindClient(t *testing.T) { hub, client, _ := setupHubTest() hub.PushScope() - newClient, _ := NewClient(ClientOptions{Dsn: testDsn, Transport: &MockTransport{}}) + newClient, _ := newClient(ClientOptions{Dsn: testDsn, Transport: &MockTransport{}}) hub.BindClient(newClient) if (*hub.stack)[0].client == (*hub.stack)[1].client { @@ -133,7 +133,7 @@ func TestWithScopeBindClient(t *testing.T) { hub, client, _ := setupHubTest() hub.WithScope(func(_ *Scope) { - newClient, _ := NewClient(ClientOptions{Dsn: testDsn, Transport: &MockTransport{}}) + newClient, _ := newClient(ClientOptions{Dsn: testDsn, Transport: &MockTransport{}}) hub.BindClient(newClient) if hub.stackTop().client != newClient { t.Error("should use newly bound client") @@ -507,23 +507,19 @@ func TestHub_Flush(t *testing.T) { } } -func TestHub_Flush_NoClient(t *testing.T) { +func TestHub_Flush_NoOpClient(t *testing.T) { hub := NewHub(nil, nil) - flushed := hub.Flush(20 * time.Millisecond) - - if flushed != false { - t.Fatalf("expected flush to be false, got %v", flushed) + if !hub.Flush(20 * time.Millisecond) { + t.Fatal("expected no-op client to flush successfully") } } -func TestHub_FlushWithCtx_NoClient(t *testing.T) { +func TestHub_FlushWithCtx_NoOpClient(t *testing.T) { hub := NewHub(nil, nil) cancelCtx, cancel := context.WithCancel(context.Background()) defer cancel() - flushed := hub.FlushWithContext(cancelCtx) - - if flushed != false { - t.Fatalf("expected flush to be false, got %v", flushed) + if !hub.FlushWithContext(cancelCtx) { + t.Fatal("expected no-op client to flush successfully") } } diff --git a/integrations.go b/integrations.go index 0d9265220..95f8c009c 100644 --- a/integrations.go +++ b/integrations.go @@ -25,7 +25,7 @@ func (mi *modulesIntegration) Name() string { return "Modules" } -func (mi *modulesIntegration) SetupOnce(client *Client) { +func (mi *modulesIntegration) SetupOnce(client Client) { client.AddEventProcessor(mi.processor) } @@ -68,7 +68,7 @@ func (ei *environmentIntegration) Name() string { return "Environment" } -func (ei *environmentIntegration) SetupOnce(client *Client) { +func (ei *environmentIntegration) SetupOnce(client Client) { client.AddEventProcessor(ei.processor) } @@ -132,8 +132,8 @@ func (iei *ignoreErrorsIntegration) Name() string { return "IgnoreErrors" } -func (iei *ignoreErrorsIntegration) SetupOnce(client *Client) { - iei.ignoreErrors = transformStringsIntoRegexps(client.options.IgnoreErrors) +func (iei *ignoreErrorsIntegration) SetupOnce(client Client) { + iei.ignoreErrors = transformStringsIntoRegexps(client.clientOptions().IgnoreErrors) client.AddEventProcessor(iei.processor) } @@ -192,8 +192,8 @@ func (iei *ignoreTransactionsIntegration) Name() string { return "IgnoreTransactions" } -func (iei *ignoreTransactionsIntegration) SetupOnce(client *Client) { - iei.ignoreTransactions = transformStringsIntoRegexps(client.options.IgnoreTransactions) +func (iei *ignoreTransactionsIntegration) SetupOnce(client Client) { + iei.ignoreTransactions = transformStringsIntoRegexps(client.clientOptions().IgnoreTransactions) client.AddEventProcessor(iei.processor) } @@ -229,9 +229,10 @@ func (ti *globalTagsIntegration) Name() string { return "GlobalTags" } -func (ti *globalTagsIntegration) SetupOnce(client *Client) { - ti.tags = make(map[string]string, len(client.options.Tags)) - for k, v := range client.options.Tags { +func (ti *globalTagsIntegration) SetupOnce(client Client) { + tags := client.clientOptions().Tags + ti.tags = make(map[string]string, len(tags)) + for k, v := range tags { ti.tags[k] = v } diff --git a/integrations_test.go b/integrations_test.go index 54ce49597..f062b7d56 100644 --- a/integrations_test.go +++ b/integrations_test.go @@ -329,7 +329,7 @@ func TestExtractModules(t *testing.T) { func TestEnvironmentIntegrationDoesNotOverrideExistingContexts(t *testing.T) { transport := &MockTransport{} - client, err := NewClient(ClientOptions{ + client, err := newClient(ClientOptions{ Transport: transport, Integrations: func([]Integration) []Integration { return []Integration{new(environmentIntegration)} @@ -381,7 +381,7 @@ func TestGlobalTagsIntegration(t *testing.T) { defer os.Unsetenv("SENTRY_TAGS_baz") transport := &MockTransport{} - client, err := NewClient(ClientOptions{ + client, err := newClient(ClientOptions{ Transport: transport, Tags: map[string]string{ "foo": "foo_value_client_options", diff --git a/interfaces.go b/interfaces.go index 4d14df65b..c65c74503 100644 --- a/interfaces.go +++ b/interfaces.go @@ -247,7 +247,7 @@ type Request struct { Env map[string]string `json:"env,omitempty"` } -func newRequest(r *http.Request, client *Client) *Request { +func newRequest(r *http.Request, client Client) *Request { prot := protocol.SchemeHTTP if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" { prot = protocol.SchemeHTTPS diff --git a/interfaces_test.go b/interfaces_test.go index 33bf33b75..11f865bdc 100644 --- a/interfaces_test.go +++ b/interfaces_test.go @@ -77,12 +77,12 @@ func TestUserMarshalJson(t *testing.T) { func TestNewRequest(t *testing.T) { t.Parallel() - makeClient := func(t *testing.T, dc *DataCollection) *Client { + makeClient := func(t *testing.T, dc *DataCollection) *defaultClient { t.Helper() if dc == nil { dc = &DataCollection{} } - client, err := NewClient(ClientOptions{ + client, err := newClient(ClientOptions{ Dsn: "https://key@sentry.io/1", DataCollection: dc, }) diff --git a/internal/sentrytest/fixture.go b/internal/sentrytest/fixture.go index 478aac283..dd1c57d16 100644 --- a/internal/sentrytest/fixture.go +++ b/internal/sentrytest/fixture.go @@ -79,7 +79,7 @@ type Fixture struct { Hub *sentry.Hub // Client is the fixture's client, configured with the MockTransport. - Client *sentry.Client + Client sentry.Client // Transport captures all events sent through the client. Transport *sentry.MockTransport @@ -243,7 +243,7 @@ func (f *Fixture) AssertHubIsolation(requestHub *sentry.Hub) { // Apply the request scope to a probe event to read its tags. probe := &sentry.Event{} - applied := requestHub.Scope().ApplyToEvent(probe, nil, nil) + applied := requestHub.Scope().ApplyToEvent(probe, nil, sentry.NewNoopClient()) if applied == nil { f.T.Error("event dropped by event processor") return diff --git a/internal/telemetry/attachments_regression_test.go b/internal/telemetry/attachments_regression_test.go index 048003062..6eddc35ca 100644 --- a/internal/telemetry/attachments_regression_test.go +++ b/internal/telemetry/attachments_regression_test.go @@ -24,7 +24,7 @@ func TestProcessorFlush_EnvelopeCarriesScopeAttachments(t *testing.T) { Payload: []byte("hello world"), }) - event = scope.ApplyToEvent(event, nil, nil) + event = scope.ApplyToEvent(event, nil, sentry.NewNoopClient()) transport := &testutils.MockTelemetryTransport{} processor := telemetry.NewProcessor( diff --git a/iris/sentryiris.go b/iris/sentryiris.go index e3e1589a2..ade474208 100644 --- a/iris/sentryiris.go +++ b/iris/sentryiris.go @@ -60,9 +60,7 @@ func (h *handler) handle(ctx iris.Context) { hub = sentry.CurrentHub().Clone() } - if client := hub.Client(); client != nil { - client.SetSDKIdentifier(sdkIdentifier) - } + hub.Client().SetSDKIdentifier(sdkIdentifier) r := ctx.Request() diff --git a/log.go b/log.go index 0279bd89f..f47cfe2f8 100644 --- a/log.go +++ b/log.go @@ -60,19 +60,20 @@ func NewLogger(ctx context.Context) Logger { // nolint: dupl } client := hub.Client() - if client != nil && !client.options.DisableLogs { + options := client.clientOptions() + if !options.DisableLogs { // Build default attrs - serverAddr := client.options.ServerName + serverAddr := options.ServerName if serverAddr == "" { serverAddr, _ = os.Hostname() } defaults := map[string]string{ - "sentry.release": client.options.Release, - "sentry.environment": client.options.Environment, + "sentry.release": options.Release, + "sentry.environment": options.Environment, "sentry.server.address": serverAddr, - "sentry.sdk.name": client.sdkIdentifier, - "sentry.sdk.version": client.sdkVersion, + "sentry.sdk.name": client.GetSDKIdentifier(), + "sentry.sdk.version": client.GetSDKVersion(), } defaultAttrs := make(map[string]attribute.Value, len(defaults)) @@ -111,10 +112,6 @@ func (l *sentryLogger) log(ctx context.Context, level LogLevel, severity int, me hub = l.hub } client := hub.Client() - if client == nil { - return - } - scope := hub.Scope() traceID, spanID := resolveTrace(scope, client, ctx, l.ctx) @@ -162,7 +159,7 @@ func (l *sentryLogger) log(ctx context.Context, level LogLevel, severity int, me log.approximateSize = computeLogSize(log) client.captureLog(log, scope) - if client.options.Debug { + if client.clientOptions().Debug { debuglog.Print(body) } } diff --git a/log_batch_processor.go b/log_batch_processor.go index e361a3937..024a7efb9 100644 --- a/log_batch_processor.go +++ b/log_batch_processor.go @@ -9,7 +9,7 @@ type logBatchProcessor struct { *batchProcessor[Log] } -func newLogBatchProcessor(client *Client) *logBatchProcessor { +func newLogBatchProcessor(client *defaultClient) *logBatchProcessor { return &logBatchProcessor{ batchProcessor: newBatchProcessor(func(items []Log) { if len(items) == 0 { diff --git a/log_race_test.go b/log_race_test.go index 1bf345ee7..dde116e74 100644 --- a/log_race_test.go +++ b/log_race_test.go @@ -78,7 +78,7 @@ func TestLoggingRaceConditions(t *testing.T) { } func testConcurrentLoggerSetAttributes(t *testing.T) { - client, _ := NewClient(ClientOptions{ + client, _ := newClient(ClientOptions{ Dsn: testDsn, Transport: &MockTransport{}, }) @@ -127,7 +127,7 @@ func testConcurrentLoggerSetAttributes(t *testing.T) { } func testConcurrentLogEmission(_ *testing.T) { - client, _ := NewClient(ClientOptions{ + client, _ := newClient(ClientOptions{ Dsn: testDsn, Transport: &MockTransport{}, }) @@ -205,7 +205,7 @@ func testConcurrentLogEmission(_ *testing.T) { func testConcurrentLogEntryOperations(t *testing.T) { t.Skip("A single instance of a log entry should not be used concurrently") - client, _ := NewClient(ClientOptions{ + client, _ := newClient(ClientOptions{ Dsn: testDsn, Transport: &MockTransport{}, }) @@ -260,7 +260,7 @@ func testConcurrentLogEntryOperations(t *testing.T) { } func testConcurrentLoggerCreationAndUsage(_ *testing.T) { - client, _ := NewClient(ClientOptions{ + client, _ := newClient(ClientOptions{ Dsn: testDsn, Transport: &MockTransport{}, }) @@ -310,7 +310,7 @@ func testConcurrentLoggerCreationAndUsage(_ *testing.T) { } func testConcurrentLogWithSpanOperations(_ *testing.T) { - client, _ := NewClient(ClientOptions{ + client, _ := newClient(ClientOptions{ Dsn: testDsn, EnableTracing: true, TracesSampleRate: 1.0, diff --git a/log_test.go b/log_test.go index 2ac39a888..e14313ced 100644 --- a/log_test.go +++ b/log_test.go @@ -33,7 +33,7 @@ func flushFromContext(ctx context.Context, timeout time.Duration) { func setupMockTransport() (context.Context, *MockTransport) { ctx := context.Background() mockTransport := &MockTransport{} - mockClient, _ := NewClient(ClientOptions{ + mockClient, _ := newClient(ClientOptions{ Dsn: testDsn, Transport: mockTransport, Release: "v1.2.3", @@ -42,7 +42,6 @@ func setupMockTransport() (context.Context, *MockTransport) { EnableTracing: true, }) mockClient.sdkIdentifier = "sentry.go" - mockClient.sdkVersion = "0.10.0" hub := CurrentHub().Clone() hub.BindClient(mockClient) hub.Scope().propagationContext.TraceID = TraceIDFromHex(LogTraceID) @@ -57,7 +56,7 @@ func Test_sentryLogger_MethodsWithFormat(t *testing.T) { "sentry.environment": attribute.StringValue("testing"), "sentry.server.address": attribute.StringValue("test-server"), "sentry.sdk.name": attribute.StringValue("sentry.go"), - "sentry.sdk.version": attribute.StringValue("0.10.0"), + "sentry.sdk.version": attribute.StringValue(SDKVersion), "sentry.message.template": attribute.StringValue("param matching: %v and %v"), "sentry.message.parameters.0": attribute.StringValue("param1"), "sentry.message.parameters.1": attribute.StringValue("param2"), @@ -218,7 +217,7 @@ func Test_sentryLogger_MethodsWithoutFormat(t *testing.T) { "sentry.environment": attribute.StringValue("testing"), "sentry.server.address": attribute.StringValue("test-server"), "sentry.sdk.name": attribute.StringValue("sentry.go"), - "sentry.sdk.version": attribute.StringValue("0.10.0"), + "sentry.sdk.version": attribute.StringValue(SDKVersion), } tests := []struct { @@ -392,7 +391,7 @@ func Test_sentryLogger_Write(t *testing.T) { "sentry.environment": attribute.StringValue("testing"), "sentry.server.address": attribute.StringValue("test-server"), "sentry.sdk.name": attribute.StringValue("sentry.go"), - "sentry.sdk.version": attribute.StringValue("0.10.0"), + "sentry.sdk.version": attribute.StringValue(SDKVersion), } wantLogs := []Log{ { @@ -682,7 +681,7 @@ func Test_batchLogger_FlushMultipleTimes(t *testing.T) { func Test_batchLogger_Shutdown(t *testing.T) { mockTransport := &MockTransport{} - mockClient, _ := NewClient(ClientOptions{ + mockClient, _ := newClient(ClientOptions{ Dsn: testDsn, Transport: mockTransport, DisableTelemetryBuffer: true, @@ -696,7 +695,7 @@ func Test_batchLogger_Shutdown(t *testing.T) { } hub = GetHubFromContext(ctx) - hub.Client().batchLogger.Shutdown() + hub.Client().(*defaultClient).batchLogger.Shutdown() events := mockTransport.Events() if len(events) != 1 { @@ -709,8 +708,8 @@ func Test_batchLogger_Shutdown(t *testing.T) { mockTransport.events = nil // Test that shutdown can be called multiple times safely - hub.Client().batchLogger.Shutdown() - hub.Client().batchLogger.Shutdown() + hub.Client().(*defaultClient).batchLogger.Shutdown() + hub.Client().(*defaultClient).batchLogger.Shutdown() events = mockTransport.Events() if len(events) != 0 { @@ -727,7 +726,7 @@ func Test_batchLogger_Shutdown(t *testing.T) { func Test_sentryLogger_BeforeSendLog(t *testing.T) { ctx := context.Background() mockTransport := &MockTransport{} - mockClient, _ := NewClient(ClientOptions{ + mockClient, _ := newClient(ClientOptions{ Dsn: testDsn, Transport: mockTransport, Release: "v1.2.3", @@ -739,7 +738,6 @@ func Test_sentryLogger_BeforeSendLog(t *testing.T) { }, }) mockClient.sdkIdentifier = "sentry.go" - mockClient.sdkVersion = "0.10.0" hub := CurrentHub() hub.BindClient(mockClient) hub.Scope().propagationContext.TraceID = TraceIDFromHex(LogTraceID) @@ -828,7 +826,7 @@ func TestSentryLogger_DebugLogging(t *testing.T) { var buf bytes.Buffer ctx := context.Background() - mockClient, _ := NewClient(ClientOptions{ + mockClient, _ := newClient(ClientOptions{ Transport: &MockTransport{}, DisableLogs: tt.disableLogs, Debug: true, @@ -857,7 +855,7 @@ func TestSentryLogger_DebugLogging(t *testing.T) { func Test_sentryLogger_UserAttributes(t *testing.T) { ctx := context.Background() mockTransport := &MockTransport{} - mockClient, _ := NewClient(ClientOptions{ + mockClient, _ := newClient(ClientOptions{ Dsn: testDsn, Transport: mockTransport, Release: "v1.2.3", @@ -866,7 +864,6 @@ func Test_sentryLogger_UserAttributes(t *testing.T) { EnableTracing: true, }) mockClient.sdkIdentifier = "sentry.go" - mockClient.sdkVersion = "0.10.0" hub := CurrentHub().Clone() hub.BindClient(mockClient) hub.Scope().propagationContext.TraceID = TraceIDFromHex(LogTraceID) diff --git a/logrus/logrusentry.go b/logrus/logrusentry.go index 838f70840..095f8f8c8 100644 --- a/logrus/logrusentry.go +++ b/logrus/logrusentry.go @@ -158,7 +158,7 @@ func (h *logHook) Fire(entry *logrus.Entry) error { hub := sentry.GetHubFromContext(ctx) if h.useCustomProvider { - if customHub := h.hubProvider(); customHub != nil && customHub.Client() != nil { + if customHub := h.hubProvider(); customHub != nil && customHub.Client().IsEnabled() { hub = customHub } } @@ -242,7 +242,7 @@ func NewLogHook(levels []logrus.Level, opts sentry.ClientOptions) (Hook, error) // NewLogHookFromClient initializes a new Logrus hook which sends logs to the provided // sentry client. -func NewLogHookFromClient(levels []logrus.Level, client *sentry.Client) Hook { +func NewLogHookFromClient(levels []logrus.Level, client sentry.Client) Hook { defaultHub := sentry.NewHub(client, sentry.NewScope()) ctx := sentry.SetHubOnContext(context.Background(), defaultHub) logger := sentry.NewLogger(ctx) diff --git a/logrus/logrusentry_test.go b/logrus/logrusentry_test.go index 8508cdd2b..74cfdb905 100644 --- a/logrus/logrusentry_test.go +++ b/logrus/logrusentry_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/assert" ) -func setupClientTest() (*sentry.Client, *sentry.MockTransport) { +func setupClientTest() (sentry.Client, *sentry.MockTransport) { mockTransport := &sentry.MockTransport{} mockClient, _ := sentry.NewClient(sentry.ClientOptions{ Dsn: "http://whatever@example.com/1337", diff --git a/metric_batch_processor.go b/metric_batch_processor.go index 788d3adfb..ea89d9b0c 100644 --- a/metric_batch_processor.go +++ b/metric_batch_processor.go @@ -9,7 +9,7 @@ type metricBatchProcessor struct { *batchProcessor[Metric] } -func newMetricBatchProcessor(client *Client) *metricBatchProcessor { +func newMetricBatchProcessor(client *defaultClient) *metricBatchProcessor { return &metricBatchProcessor{ batchProcessor: newBatchProcessor(func(items []Metric) { if len(items) == 0 { diff --git a/metrics.go b/metrics.go index 3a055d88d..3ef82817e 100644 --- a/metrics.go +++ b/metrics.go @@ -47,27 +47,28 @@ const ( UnitPercent = "percent" ) -// NewMeter returns a new Meter. If there is no Client bound to the current hub, or if metrics are disabled, -// it returns a no-op Meter that discards all metrics. +// NewMeter returns a new Meter. If the current Client is a no-op client or +// metrics are disabled, it returns a no-op Meter that discards all metrics. func NewMeter(ctx context.Context) Meter { hub := GetHubFromContext(ctx) if hub == nil { hub = CurrentHub() } client := hub.Client() - if client != nil && !client.options.DisableMetrics { + options := client.clientOptions() + if !options.DisableMetrics { // build default attrs - serverAddr := client.options.ServerName + serverAddr := options.ServerName if serverAddr == "" { serverAddr, _ = os.Hostname() } defaults := map[string]string{ - "sentry.release": client.options.Release, - "sentry.environment": client.options.Environment, + "sentry.release": options.Release, + "sentry.environment": options.Environment, "sentry.server.address": serverAddr, - "sentry.sdk.name": client.sdkIdentifier, - "sentry.sdk.version": client.sdkVersion, + "sentry.sdk.name": client.GetSDKIdentifier(), + "sentry.sdk.version": client.GetSDKVersion(), } defaultAttrs := make(map[string]attribute.Value) @@ -110,10 +111,6 @@ func (m *sentryMeter) emit(ctx context.Context, metricType MetricType, name stri } client := hub.Client() - if client == nil { - return - } - scope := hub.Scope() if customScope != nil { scope = customScope @@ -151,7 +148,7 @@ func (m *sentryMeter) emit(ctx context.Context, metricType MetricType, name stri Attributes: attrs, } - if client.captureMetric(metric, scope) && client.options.Debug { + if client.captureMetric(metric, scope) && client.clientOptions().Debug { debuglog.Printf("Metric %s [%s]: %v %s", metricType, name, value.AsInterface(), unit) } } diff --git a/metrics_test.go b/metrics_test.go index ced5552fa..51ad346d8 100644 --- a/metrics_test.go +++ b/metrics_test.go @@ -15,7 +15,7 @@ import ( func setupMetricsTest() (context.Context, *MockTransport) { ctx := context.Background() mockTransport := &MockTransport{} - mockClient, _ := NewClient(ClientOptions{ + mockClient, _ := newClient(ClientOptions{ Dsn: testDsn, Transport: mockTransport, Release: "v1.2.3", @@ -24,7 +24,6 @@ func setupMetricsTest() (context.Context, *MockTransport) { EnableTracing: true, }) mockClient.sdkIdentifier = "sentry.go" - mockClient.sdkVersion = "0.10.0" hub := CurrentHub().Clone() hub.BindClient(mockClient) hub.Scope().propagationContext.TraceID = TraceIDFromHex(LogTraceID) @@ -39,7 +38,7 @@ func Test_sentryMeter_Methods(t *testing.T) { "sentry.environment": attribute.StringValue("testing"), "sentry.server.address": attribute.StringValue("test-server"), "sentry.sdk.name": attribute.StringValue("sentry.go"), - "sentry.sdk.version": attribute.StringValue("0.10.0"), + "sentry.sdk.version": attribute.StringValue(SDKVersion), } tests := []struct { @@ -308,7 +307,7 @@ func Test_batchMeter_FlushWithContext(t *testing.T) { func Test_sentryMeter_BeforeSendMetric(t *testing.T) { ctx := context.Background() mockTransport := &MockTransport{} - mockClient, _ := NewClient(ClientOptions{ + mockClient, _ := newClient(ClientOptions{ Dsn: testDsn, Transport: mockTransport, Release: "v1.2.3", @@ -320,7 +319,6 @@ func Test_sentryMeter_BeforeSendMetric(t *testing.T) { }, }) mockClient.sdkIdentifier = "sentry.go" - mockClient.sdkVersion = "0.10.0" hub := CurrentHub().Clone() hub.BindClient(mockClient) hub.Scope().propagationContext.TraceID = TraceIDFromHex(LogTraceID) @@ -396,7 +394,7 @@ func Test_batchMeter_FlushMultipleTimes(t *testing.T) { func Test_batchMeter_Shutdown(t *testing.T) { mockTransport := &MockTransport{} - mockClient, _ := NewClient(ClientOptions{ + mockClient, _ := newClient(ClientOptions{ Dsn: testDsn, Transport: mockTransport, DisableTelemetryBuffer: true, @@ -410,7 +408,7 @@ func Test_batchMeter_Shutdown(t *testing.T) { } hub = GetHubFromContext(ctx) - hub.Client().batchMeter.Shutdown() + hub.Client().(*defaultClient).batchMeter.Shutdown() events := mockTransport.Events() if len(events) != 1 { @@ -423,8 +421,8 @@ func Test_batchMeter_Shutdown(t *testing.T) { mockTransport.events = nil // Test that shutdown can be called multiple times safely - hub.Client().batchMeter.Shutdown() - hub.Client().batchMeter.Shutdown() + hub.Client().(*defaultClient).batchMeter.Shutdown() + hub.Client().(*defaultClient).batchMeter.Shutdown() events = mockTransport.Events() if len(events) != 0 { @@ -475,7 +473,7 @@ func Test_sentryMeter_TracePropagationWithTransaction(t *testing.T) { func Test_sentryMeter_UserAttributes(t *testing.T) { ctx := context.Background() mockTransport := &MockTransport{} - mockClient, _ := NewClient(ClientOptions{ + mockClient, _ := newClient(ClientOptions{ Dsn: testDsn, Transport: mockTransport, Release: "v1.2.3", @@ -484,7 +482,6 @@ func Test_sentryMeter_UserAttributes(t *testing.T) { EnableTracing: true, }) mockClient.sdkIdentifier = "sentry.go" - mockClient.sdkVersion = "0.10.0" hub := CurrentHub().Clone() hub.BindClient(mockClient) hub.Scope().propagationContext.TraceID = TraceIDFromHex(LogTraceID) @@ -650,7 +647,7 @@ func Test_sentryMeter_AttributePrecedence(t *testing.T) { func TestNewMeter_DisabledMetrics(t *testing.T) { ctx := context.Background() mockTransport := &MockTransport{} - mockClient, _ := NewClient(ClientOptions{ + mockClient, _ := newClient(ClientOptions{ Dsn: testDsn, Transport: mockTransport, DisableMetrics: true, diff --git a/mocks.go b/mocks.go index 9492b1c24..ad252b596 100644 --- a/mocks.go +++ b/mocks.go @@ -16,7 +16,7 @@ func (scope *MockScope) AddBreadcrumb(breadcrumb *Breadcrumb, _ int) { scope.breadcrumb = breadcrumb } -func (scope *MockScope) ApplyToEvent(event *Event, _ *EventHint, _ *Client) *Event { +func (scope *MockScope) ApplyToEvent(event *Event, _ *EventHint, _ Client) *Event { if scope.shouldDropEvent { return nil } diff --git a/negroni/sentrynegroni.go b/negroni/sentrynegroni.go index cdee10f88..2599379bb 100644 --- a/negroni/sentrynegroni.go +++ b/negroni/sentrynegroni.go @@ -52,9 +52,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.Ha hub = sentry.CurrentHub().Clone() } - if client := hub.Client(); client != nil { - client.SetSDKIdentifier(sdkIdentifier) - } + hub.Client().SetSDKIdentifier(sdkIdentifier) options := []sentry.SpanOption{ sentry.ContinueTrace(hub, r.Header.Get(sentry.SentryTraceHeader), r.Header.Get(sentry.SentryBaggageHeader)), diff --git a/otel/linking_integration.go b/otel/linking_integration.go index ca81adbac..9f8057ae4 100644 --- a/otel/linking_integration.go +++ b/otel/linking_integration.go @@ -19,6 +19,6 @@ func (integration) Name() string { return "OTel" } -func (integration) SetupOnce(client *sentry.Client) { +func (integration) SetupOnce(client sentry.Client) { client.SetExternalContextTraceResolver(common.ResolveTraceContext) } diff --git a/recover_external_test.go b/recover_external_test.go index 739ea1063..d4c45a695 100644 --- a/recover_external_test.go +++ b/recover_external_test.go @@ -43,7 +43,7 @@ func panicWithArbitraryValue() { } //go:noinline -func recoverPanic(client *sentry.Client, panicFunc func()) { +func recoverPanic(client sentry.Client, panicFunc func()) { defer func() { if err := recover(); err != nil { client.Recover(err, nil, nil) @@ -54,11 +54,11 @@ func recoverPanic(client *sentry.Client, panicFunc func()) { } //go:noinline -func recoverHandledError(client *sentry.Client) { +func recoverHandledError(client sentry.Client) { client.Recover(errors.New("boom"), nil, nil) } -func newRecoverTestClient(t *testing.T) (*sentry.Client, *sentry.MockTransport) { +func newRecoverTestClient(t *testing.T) (sentry.Client, *sentry.MockTransport) { t.Helper() transport := &sentry.MockTransport{} diff --git a/scope.go b/scope.go index c7a60676b..863b7fd04 100644 --- a/scope.go +++ b/scope.go @@ -314,7 +314,9 @@ func (scope *Scope) AddEventProcessor(processor EventProcessor) { } // ApplyToEvent takes the data from the current scope and attaches it to the event. -func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint, client *Client) *Event { //nolint:gocyclo +func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint, client Client) *Event { //nolint:gocyclo + client = normalizeClient(client) + scope.mu.RLock() defer scope.mu.RUnlock() @@ -375,7 +377,7 @@ func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint, client *Client) event.Contexts["trace"] = scope.propagationContext.Map() dsc := scope.propagationContext.DynamicSamplingContext - if !dsc.HasEntries() && client != nil { + if !dsc.HasEntries() { dsc = DynamicSamplingContextFromScope(scope, client) } event.sdkMetaData.dsc = dsc @@ -383,19 +385,17 @@ func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint, client *Client) // If an external trace resolver is registered (e.g. OTel), override // trace/span IDs from the hint context or the scope's request context. - if client != nil { - var ctx context.Context - if hint != nil { - ctx = hint.Context - } - if ctx == nil && scope.request != nil { - ctx = scope.request.Context() - } - if traceID, spanID, ok := client.externalTraceContextFromContext(ctx); event.Type != transactionType && ok { - traceCtx := event.Contexts["trace"] - traceCtx["trace_id"] = traceID.String() - traceCtx["span_id"] = spanID.String() - } + var ctx context.Context + if hint != nil { + ctx = hint.Context + } + if ctx == nil && scope.request != nil { + ctx = scope.request.Context() + } + if traceID, spanID, ok := client.externalTraceContextFromContext(ctx); event.Type != transactionType && ok { + traceCtx := event.Contexts["trace"] + traceCtx["trace_id"] = traceID.String() + traceCtx["span_id"] = spanID.String() } if event.User.IsEmpty() { @@ -434,18 +434,14 @@ func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint, client *Client) event = processor(event, hint) if event == nil { debuglog.Printf("Event dropped by one of the Scope EventProcessors: %s\n", id) - if client != nil { - client.reportRecorder.RecordOne(report.ReasonEventProcessor, category) - if category == ratelimit.CategoryTransaction { - client.reportRecorder.Record(report.ReasonEventProcessor, ratelimit.CategorySpan, int64(spanCountBefore)) - } + client.recordDiscard(report.ReasonEventProcessor, category, 1) + if category == ratelimit.CategoryTransaction { + client.recordDiscard(report.ReasonEventProcessor, ratelimit.CategorySpan, int64(spanCountBefore)) } return nil } if droppedSpans := spanCountBefore - event.GetSpanCount(); droppedSpans > 0 { - if client != nil { - client.reportRecorder.Record(report.ReasonEventProcessor, ratelimit.CategorySpan, int64(droppedSpans)) - } + client.recordDiscard(report.ReasonEventProcessor, ratelimit.CategorySpan, int64(droppedSpans)) } } @@ -517,17 +513,16 @@ func hubFromContexts(ctxs ...context.Context) *Hub { // This ordering ensures we always use the most contextually relevant tracing information. // For example, if a specific span is active for an operation, we use that span's trace/span IDs // rather than accidentally using a different span that might be set on the hub's scope. -func resolveTrace(scope *Scope, client *Client, ctxs ...context.Context) (traceID TraceID, spanID SpanID) { +func resolveTrace(scope *Scope, client Client, ctxs ...context.Context) (traceID TraceID, spanID SpanID) { + client = normalizeClient(client) var span *Span for _, ctx := range ctxs { if ctx == nil { continue } - if client != nil { - if traceID, spanID, ok := client.externalTraceContextFromContext(ctx); ok { - return traceID, spanID - } + if traceID, spanID, ok := client.externalTraceContextFromContext(ctx); ok { + return traceID, spanID } if span = SpanFromContext(ctx); span != nil { break diff --git a/scope_test.go b/scope_test.go index da81ca6b8..aa8293bf1 100644 --- a/scope_test.go +++ b/scope_test.go @@ -451,15 +451,15 @@ func TestScopeCloneEventProcessorsIsolation(t *testing.T) { clone2.AddEventProcessor(mark("y")) ran = nil - clone1.ApplyToEvent(NewEvent(), nil, nil) + clone1.ApplyToEvent(NewEvent(), nil, noopClient{}) assertEqual(t, []string{"a", "b", "c", "x"}, ran) ran = nil - clone2.ApplyToEvent(NewEvent(), nil, nil) + clone2.ApplyToEvent(NewEvent(), nil, noopClient{}) assertEqual(t, []string{"a", "b", "c", "y"}, ran) ran = nil - scope.ApplyToEvent(NewEvent(), nil, nil) + scope.ApplyToEvent(NewEvent(), nil, noopClient{}) assertEqual(t, []string{"a", "b", "c"}, ran) } @@ -603,7 +603,7 @@ func TestApplyToEventWithCorrectScopeAndEvent(t *testing.T) { scope := fillScopeWithData(NewScope()) event := fillEventWithData(NewEvent()) - processedEvent := scope.ApplyToEvent(event, nil, nil) + processedEvent := scope.ApplyToEvent(event, nil, noopClient{}) assertEqual(t, 2, len(processedEvent.Breadcrumbs), "should merge breadcrumbs") assertEqual(t, 2, len(processedEvent.Attachments), "should merge attachments") @@ -623,7 +623,7 @@ func TestApplyToEventUsingEmptyScope(t *testing.T) { scope := NewScope() event := fillEventWithData(NewEvent()) - processedEvent := scope.ApplyToEvent(event, nil, nil) + processedEvent := scope.ApplyToEvent(event, nil, noopClient{}) assertEqual(t, len(processedEvent.Breadcrumbs), 1, "should use event breadcrumbs") assertEqual(t, len(processedEvent.Attachments), 1, "should use event attachments") assertEqual(t, len(processedEvent.Tags), 1, "should use event tags") @@ -638,7 +638,7 @@ func TestApplyToEventUsingEmptyEvent(t *testing.T) { scope := fillScopeWithData(NewScope()) event := NewEvent() - processedEvent := scope.ApplyToEvent(event, nil, nil) + processedEvent := scope.ApplyToEvent(event, nil, noopClient{}) assertEqual(t, len(processedEvent.Breadcrumbs), 1, "should use scope breadcrumbs") assertEqual(t, len(processedEvent.Attachments), 1, "should use scope attachments") assertEqual(t, len(processedEvent.Tags), 1, "should use scope tags") @@ -651,7 +651,7 @@ func TestApplyToEventUsingEmptyEvent(t *testing.T) { func TestApplyToEventUsesClientPIISettingsForRequest(t *testing.T) { previousClient := currentHub.Client() - currentClient, err := NewClient(ClientOptions{Dsn: "https://key@sentry.io/1", SendDefaultPII: true}) + currentClient, err := newClient(ClientOptions{Dsn: "https://key@sentry.io/1", SendDefaultPII: true}) if err != nil { t.Fatal(err) } @@ -665,7 +665,7 @@ func TestApplyToEventUsesClientPIISettingsForRequest(t *testing.T) { request.Header.Set("Some-Header", "some-header value") scope.SetRequest(request) - requestClient, err := NewClient(ClientOptions{ + requestClient, err := newClient(ClientOptions{ Dsn: "https://key@sentry.io/1", DataCollection: &DataCollection{ UserInfo: Set(false), @@ -699,7 +699,7 @@ func TestApplyToEventUsesClientPIISettingsForRequest(t *testing.T) { } func TestApplyToEventUsesExplicitDataCollectionForRequest(t *testing.T) { - client, err := NewClient(ClientOptions{ + client, err := newClient(ClientOptions{ Dsn: "https://key@sentry.io/1", DataCollection: &DataCollection{ UserInfo: Set(false), @@ -755,7 +755,7 @@ func TestApplyToEventHTTPBodyCollection(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - client, err := NewClient(ClientOptions{ + client, err := newClient(ClientOptions{ Dsn: "https://key@sentry.io/1", DataCollection: &DataCollection{HTTPBodies: tt.bodies}, }) @@ -792,7 +792,7 @@ func TestEventProcessorsModifiesEvent(t *testing.T) { return event }, } - processedEvent := scope.ApplyToEvent(event, nil, nil) + processedEvent := scope.ApplyToEvent(event, nil, noopClient{}) if processedEvent == nil { t.Fatal("event should not be dropped") @@ -809,7 +809,7 @@ func TestEventProcessorsCanDropEvent(t *testing.T) { return nil }, } - processedEvent := scope.ApplyToEvent(event, nil, nil) + processedEvent := scope.ApplyToEvent(event, nil, noopClient{}) if processedEvent != nil { t.Error("event should be dropped") @@ -819,7 +819,7 @@ func TestEventProcessorsCanDropEvent(t *testing.T) { func TestEventProcessorsAddEventProcessor(t *testing.T) { scope := NewScope() event := NewEvent() - processedEvent := scope.ApplyToEvent(event, nil, nil) + processedEvent := scope.ApplyToEvent(event, nil, noopClient{}) if processedEvent == nil { t.Error("event should not be dropped") @@ -828,7 +828,7 @@ func TestEventProcessorsAddEventProcessor(t *testing.T) { scope.AddEventProcessor(func(_ *Event, _ *EventHint) *Event { return nil }) - processedEvent = scope.ApplyToEvent(event, nil, nil) + processedEvent = scope.ApplyToEvent(event, nil, noopClient{}) if processedEvent != nil { t.Error("event should be dropped") diff --git a/span_recorder.go b/span_recorder.go index ba0410150..ba3037899 100644 --- a/span_recorder.go +++ b/span_recorder.go @@ -17,9 +17,11 @@ type spanRecorder struct { // record stores a span. The first stored span is assumed to be the root of a // span tree. func (r *spanRecorder) record(s *Span) { - maxSpans := defaultMaxSpans - if client := CurrentHub().Client(); client != nil { - maxSpans = client.options.MaxSpans + maxSpans := CurrentHub().Client().clientOptions().MaxSpans + if maxSpans == 0 { + // MaxSpans is only zero for the no-op client: NewClient defaults it + // to defaultMaxSpans. + maxSpans = defaultMaxSpans } r.mu.Lock() defer r.mu.Unlock() diff --git a/span_recorder_test.go b/span_recorder_test.go index f8d8706ff..2c7117508 100644 --- a/span_recorder_test.go +++ b/span_recorder_test.go @@ -38,7 +38,7 @@ func Test_spanRecorder_record(t *testing.T) { defer debuglog.SetOutput(io.Discard) spanRecorder := spanRecorder{} - currentHub.BindClient(&Client{ + currentHub.BindClient(&defaultClient{ options: ClientOptions{ MaxSpans: tt.maxSpans, }, diff --git a/tracing.go b/tracing.go index 475a8a4a1..c11d1bf9a 100644 --- a/tracing.go +++ b/tracing.go @@ -480,17 +480,15 @@ func (s *Span) doFinish() { } if !s.Sampled.Bool() { - c := hub.Client() - if c != nil { - if !s.IsTransaction() { - // we count the sampled spans from the transaction root. it is guaranteed that the whole transaction - // would be sampled - return - } - children := s.recorder.children() - c.reportRecorder.RecordOne(report.ReasonSampleRate, ratelimit.CategoryTransaction) - c.reportRecorder.Record(report.ReasonSampleRate, ratelimit.CategorySpan, int64(len(children)+1)) + if !s.IsTransaction() { + // we count the sampled spans from the transaction root. it is guaranteed that the whole transaction + // would be sampled + return } + client := hub.Client() + children := s.recorder.children() + client.recordDiscard(report.ReasonSampleRate, ratelimit.CategoryTransaction, 1) + client.recordDiscard(report.ReasonSampleRate, ratelimit.CategorySpan, int64(len(children)+1)) return } event := s.toEvent() @@ -550,11 +548,7 @@ func (s *Span) updateFromBaggage(header []byte) { } func (s *Span) clientOptions() *ClientOptions { - client := hubFromContext(s.ctx).Client() - if client != nil { - return &client.options - } - return &ClientOptions{} + return hubFromContext(s.ctx).Client().clientOptions() } func (s *Span) sample() Sampled { @@ -1201,14 +1195,10 @@ func HTTPtoSpanStatus(code int) SpanStatus { return SpanStatusUnknown } -func shouldContinueTrace(client *Client, dsc DynamicSamplingContext) bool { - if client == nil { - return true - } - +func shouldContinueTrace(client Client, dsc DynamicSamplingContext) bool { var sdkOrgID uint64 - if client.dsn != nil { - sdkOrgID = client.dsn.GetOrgID() + if dsn := client.getDsn(); dsn != nil { + sdkOrgID = dsn.GetOrgID() } baggageOrgStr := dsc.Entries["org_id"] @@ -1223,7 +1213,7 @@ func shouldContinueTrace(client *Client, dsc DynamicSamplingContext) bool { } // If strict mode is on, both must be present and match - if client.options.StrictTraceContinuation { + if client.clientOptions().StrictTraceContinuation { if sdkOrgID == 0 && baggageOrgID == 0 { return true } diff --git a/tracing_test.go b/tracing_test.go index eb34b0b18..5f08e6f8d 100644 --- a/tracing_test.go +++ b/tracing_test.go @@ -396,7 +396,7 @@ func NewTestContext(options ClientOptions) context.Context { if options.Transport == nil { options.Transport = &MockTransport{} } - client, err := NewClient(options) + client, err := newClient(options) if err != nil { panic(err) } @@ -1293,7 +1293,7 @@ func TestSpanFinishConcurrentlyWithoutRaces(_ *testing.T) { func TestSpanScopeManagement(t *testing.T) { // Initialize a test hub and client transport := &MockTransport{} - client, err := NewClient(ClientOptions{ + client, err := newClient(ClientOptions{ EnableTracing: true, TracesSampleRate: 1.0, Transport: transport,