1package sentry
2
3import (
4	"context"
5	"crypto/x509"
6	"errors"
7	"fmt"
8	"io"
9	"io/ioutil"
10	"log"
11	"math/rand"
12	"net/http"
13	"os"
14	"reflect"
15	"sort"
16	"strings"
17	"sync"
18	"time"
19
20	"github.com/getsentry/sentry-go/internal/debug"
21)
22
23// maxErrorDepth is the maximum number of errors reported in a chain of errors.
24// This protects the SDK from an arbitrarily long chain of wrapped errors.
25//
26// An additional consideration is that arguably reporting a long chain of errors
27// is of little use when debugging production errors with Sentry. The Sentry UI
28// is not optimized for long chains either. The top-level error together with a
29// stack trace is often the most useful information.
30const maxErrorDepth = 10
31
32// hostname is the host name reported by the kernel. It is precomputed once to
33// avoid syscalls when capturing events.
34//
35// The error is ignored because retrieving the host name is best-effort. If the
36// error is non-nil, there is nothing to do other than retrying. We choose not
37// to retry for now.
38var hostname, _ = os.Hostname()
39
40// lockedRand is a random number generator safe for concurrent use. Its API is
41// intentionally limited and it is not meant as a full replacement for a
42// rand.Rand.
43type lockedRand struct {
44	mu sync.Mutex
45	r  *rand.Rand
46}
47
48// Float64 returns a pseudo-random number in [0.0,1.0).
49func (r *lockedRand) Float64() float64 {
50	r.mu.Lock()
51	defer r.mu.Unlock()
52	return r.r.Float64()
53}
54
55// rng is the internal random number generator.
56//
57// We do not use the global functions from math/rand because, while they are
58// safe for concurrent use, any package in a build could change the seed and
59// affect the generated numbers, for instance making them deterministic. On the
60// other hand, the source returned from rand.NewSource is not safe for
61// concurrent use, so we need to couple its use with a sync.Mutex.
62var rng = &lockedRand{
63	r: rand.New(rand.NewSource(time.Now().UnixNano())),
64}
65
66// usageError is used to report to Sentry an SDK usage error.
67//
68// It is not exported because it is never returned by any function or method in
69// the exported API.
70type usageError struct {
71	error
72}
73
74// Logger is an instance of log.Logger that is use to provide debug information about running Sentry Client
75// can be enabled by either using Logger.SetOutput directly or with Debug client option.
76var Logger = log.New(ioutil.Discard, "[Sentry] ", log.LstdFlags)
77
78// EventProcessor is a function that processes an event.
79// Event processors are used to change an event before it is sent to Sentry.
80type EventProcessor func(event *Event, hint *EventHint) *Event
81
82// EventModifier is the interface that wraps the ApplyToEvent method.
83//
84// ApplyToEvent changes an event based on external data and/or
85// an event hint.
86type EventModifier interface {
87	ApplyToEvent(event *Event, hint *EventHint) *Event
88}
89
90var globalEventProcessors []EventProcessor
91
92// AddGlobalEventProcessor adds processor to the global list of event
93// processors. Global event processors apply to all events.
94//
95// AddGlobalEventProcessor is deprecated. Most users will prefer to initialize
96// the SDK with Init and provide a ClientOptions.BeforeSend function or use
97// Scope.AddEventProcessor instead.
98func AddGlobalEventProcessor(processor EventProcessor) {
99	globalEventProcessors = append(globalEventProcessors, processor)
100}
101
102// Integration allows for registering a functions that modify or discard captured events.
103type Integration interface {
104	Name() string
105	SetupOnce(client *Client)
106}
107
108// ClientOptions that configures a SDK Client.
109type ClientOptions struct {
110	// The DSN to use. If the DSN is not set, the client is effectively
111	// disabled.
112	Dsn string
113	// In debug mode, the debug information is printed to stdout to help you
114	// understand what sentry is doing.
115	Debug bool
116	// Configures whether SDK should generate and attach stacktraces to pure
117	// capture message calls.
118	AttachStacktrace bool
119	// The sample rate for event submission in the range [0.0, 1.0]. By default,
120	// all events are sent. Thus, as a historical special case, the sample rate
121	// 0.0 is treated as if it was 1.0. To drop all events, set the DSN to the
122	// empty string.
123	SampleRate float64
124	// The sample rate for sampling traces in the range [0.0, 1.0].
125	TracesSampleRate float64
126	// Used to customize the sampling of traces, overrides TracesSampleRate.
127	TracesSampler TracesSampler
128	// List of regexp strings that will be used to match against event's message
129	// and if applicable, caught errors type and value.
130	// If the match is found, then a whole event will be dropped.
131	IgnoreErrors []string
132	// Before send callback.
133	BeforeSend func(event *Event, hint *EventHint) *Event
134	// Before breadcrumb add callback.
135	BeforeBreadcrumb func(breadcrumb *Breadcrumb, hint *BreadcrumbHint) *Breadcrumb
136	// Integrations to be installed on the current Client, receives default
137	// integrations.
138	Integrations func([]Integration) []Integration
139	// io.Writer implementation that should be used with the Debug mode.
140	DebugWriter io.Writer
141	// The transport to use. Defaults to HTTPTransport.
142	Transport Transport
143	// The server name to be reported.
144	ServerName string
145	// The release to be sent with events.
146	Release string
147	// The dist to be sent with events.
148	Dist string
149	// The environment to be sent with events.
150	Environment string
151	// Maximum number of breadcrumbs.
152	MaxBreadcrumbs int
153	// An optional pointer to http.Client that will be used with a default
154	// HTTPTransport. Using your own client will make HTTPTransport, HTTPProxy,
155	// HTTPSProxy and CaCerts options ignored.
156	HTTPClient *http.Client
157	// An optional pointer to http.Transport that will be used with a default
158	// HTTPTransport. Using your own transport will make HTTPProxy, HTTPSProxy
159	// and CaCerts options ignored.
160	HTTPTransport http.RoundTripper
161	// An optional HTTP proxy to use.
162	// This will default to the HTTP_PROXY environment variable.
163	HTTPProxy string
164	// An optional HTTPS proxy to use.
165	// This will default to the HTTPS_PROXY environment variable.
166	// HTTPS_PROXY takes precedence over HTTP_PROXY for https requests.
167	HTTPSProxy string
168	// An optional set of SSL certificates to use.
169	CaCerts *x509.CertPool
170}
171
172// Client is the underlying processor that is used by the main API and Hub
173// instances. It must be created with NewClient.
174type Client struct {
175	options         ClientOptions
176	dsn             *Dsn
177	eventProcessors []EventProcessor
178	integrations    []Integration
179	// Transport is read-only. Replacing the transport of an existing client is
180	// not supported, create a new client instead.
181	Transport Transport
182}
183
184// NewClient creates and returns an instance of Client configured using
185// ClientOptions.
186//
187// Most users will not create clients directly. Instead, initialize the SDK with
188// Init and use the package-level functions (for simple programs that run on a
189// single goroutine) or hub methods (for concurrent programs, for example web
190// servers).
191func NewClient(options ClientOptions) (*Client, error) {
192	if options.TracesSampleRate != 0.0 && options.TracesSampler != nil {
193		return nil, errors.New("TracesSampleRate and TracesSampler are mutually exclusive")
194	}
195
196	if options.Debug {
197		debugWriter := options.DebugWriter
198		if debugWriter == nil {
199			debugWriter = os.Stderr
200		}
201		Logger.SetOutput(debugWriter)
202	}
203
204	if options.Dsn == "" {
205		options.Dsn = os.Getenv("SENTRY_DSN")
206	}
207
208	if options.Release == "" {
209		options.Release = os.Getenv("SENTRY_RELEASE")
210	}
211
212	if options.Environment == "" {
213		options.Environment = os.Getenv("SENTRY_ENVIRONMENT")
214	}
215
216	// SENTRYGODEBUG is a comma-separated list of key=value pairs (similar
217	// to GODEBUG). It is not a supported feature: recognized debug options
218	// may change any time.
219	//
220	// The intended public is SDK developers. It is orthogonal to
221	// options.Debug, which is also available for SDK users.
222	dbg := strings.Split(os.Getenv("SENTRYGODEBUG"), ",")
223	sort.Strings(dbg)
224	// dbgOpt returns true when the given debug option is enabled, for
225	// example SENTRYGODEBUG=someopt=1.
226	dbgOpt := func(opt string) bool {
227		s := opt + "=1"
228		return dbg[sort.SearchStrings(dbg, s)%len(dbg)] == s
229	}
230	if dbgOpt("httpdump") || dbgOpt("httptrace") {
231		options.HTTPTransport = &debug.Transport{
232			RoundTripper: http.DefaultTransport,
233			Output:       os.Stderr,
234			Dump:         dbgOpt("httpdump"),
235			Trace:        dbgOpt("httptrace"),
236		}
237	}
238
239	var dsn *Dsn
240	if options.Dsn != "" {
241		var err error
242		dsn, err = NewDsn(options.Dsn)
243		if err != nil {
244			return nil, err
245		}
246	}
247
248	client := Client{
249		options: options,
250		dsn:     dsn,
251	}
252
253	client.setupTransport()
254	client.setupIntegrations()
255
256	return &client, nil
257}
258
259func (client *Client) setupTransport() {
260	opts := client.options
261	transport := opts.Transport
262
263	if transport == nil {
264		if opts.Dsn == "" {
265			transport = new(noopTransport)
266		} else {
267			httpTransport := NewHTTPTransport()
268			// When tracing is enabled, use larger buffer to
269			// accommodate more concurrent events.
270			// TODO(tracing): consider using separate buffers per
271			// event type.
272			if opts.TracesSampleRate != 0 || opts.TracesSampler != nil {
273				httpTransport.BufferSize = 1000
274			}
275			transport = httpTransport
276		}
277	}
278
279	transport.Configure(opts)
280	client.Transport = transport
281}
282
283func (client *Client) setupIntegrations() {
284	integrations := []Integration{
285		new(contextifyFramesIntegration),
286		new(environmentIntegration),
287		new(modulesIntegration),
288		new(ignoreErrorsIntegration),
289	}
290
291	if client.options.Integrations != nil {
292		integrations = client.options.Integrations(integrations)
293	}
294
295	for _, integration := range integrations {
296		if client.integrationAlreadyInstalled(integration.Name()) {
297			Logger.Printf("Integration %s is already installed\n", integration.Name())
298			continue
299		}
300		client.integrations = append(client.integrations, integration)
301		integration.SetupOnce(client)
302		Logger.Printf("Integration installed: %s\n", integration.Name())
303	}
304}
305
306// AddEventProcessor adds an event processor to the client. It must not be
307// called from concurrent goroutines. Most users will prefer to use
308// ClientOptions.BeforeSend or Scope.AddEventProcessor instead.
309//
310// Note that typical programs have only a single client created by Init and the
311// client is shared among multiple hubs, one per goroutine, such that adding an
312// event processor to the client affects all hubs that share the client.
313func (client *Client) AddEventProcessor(processor EventProcessor) {
314	client.eventProcessors = append(client.eventProcessors, processor)
315}
316
317// Options return ClientOptions for the current Client.
318func (client Client) Options() ClientOptions {
319	return client.options
320}
321
322// CaptureMessage captures an arbitrary message.
323func (client *Client) CaptureMessage(message string, hint *EventHint, scope EventModifier) *EventID {
324	event := client.eventFromMessage(message, LevelInfo)
325	return client.CaptureEvent(event, hint, scope)
326}
327
328// CaptureException captures an error.
329func (client *Client) CaptureException(exception error, hint *EventHint, scope EventModifier) *EventID {
330	event := client.eventFromException(exception, LevelError)
331	return client.CaptureEvent(event, hint, scope)
332}
333
334// CaptureEvent captures an event on the currently active client if any.
335//
336// The event must already be assembled. Typically code would instead use
337// the utility methods like CaptureException. The return value is the
338// event ID. In case Sentry is disabled or event was dropped, the return value will be nil.
339func (client *Client) CaptureEvent(event *Event, hint *EventHint, scope EventModifier) *EventID {
340	return client.processEvent(event, hint, scope)
341}
342
343// Recover captures a panic.
344// Returns EventID if successfully, or nil if there's no error to recover from.
345func (client *Client) Recover(err interface{}, hint *EventHint, scope EventModifier) *EventID {
346	if err == nil {
347		err = recover()
348	}
349
350	// Normally we would not pass a nil Context, but RecoverWithContext doesn't
351	// use the Context for communicating deadline nor cancelation. All it does
352	// is store the Context in the EventHint and there nil means the Context is
353	// not available.
354	//nolint: staticcheck
355	return client.RecoverWithContext(nil, err, hint, scope)
356}
357
358// RecoverWithContext captures a panic and passes relevant context object.
359// Returns EventID if successfully, or nil if there's no error to recover from.
360func (client *Client) RecoverWithContext(
361	ctx context.Context,
362	err interface{},
363	hint *EventHint,
364	scope EventModifier,
365) *EventID {
366	if err == nil {
367		err = recover()
368	}
369	if err == nil {
370		return nil
371	}
372
373	if ctx != nil {
374		if hint == nil {
375			hint = &EventHint{}
376		}
377		if hint.Context == nil {
378			hint.Context = ctx
379		}
380	}
381
382	var event *Event
383	switch err := err.(type) {
384	case error:
385		event = client.eventFromException(err, LevelFatal)
386	case string:
387		event = client.eventFromMessage(err, LevelFatal)
388	default:
389		event = client.eventFromMessage(fmt.Sprintf("%#v", err), LevelFatal)
390	}
391	return client.CaptureEvent(event, hint, scope)
392}
393
394// Flush waits until the underlying Transport sends any buffered events to the
395// Sentry server, blocking for at most the given timeout. It returns false if
396// the timeout was reached. In that case, some events may not have been sent.
397//
398// Flush should be called before terminating the program to avoid
399// unintentionally dropping events.
400//
401// Do not call Flush indiscriminately after every call to CaptureEvent,
402// CaptureException or CaptureMessage. Instead, to have the SDK send events over
403// the network synchronously, configure it to use the HTTPSyncTransport in the
404// call to Init.
405func (client *Client) Flush(timeout time.Duration) bool {
406	return client.Transport.Flush(timeout)
407}
408
409func (client *Client) eventFromMessage(message string, level Level) *Event {
410	if message == "" {
411		err := usageError{fmt.Errorf("%s called with empty message", callerFunctionName())}
412		return client.eventFromException(err, level)
413	}
414	event := NewEvent()
415	event.Level = level
416	event.Message = message
417
418	if client.Options().AttachStacktrace {
419		event.Threads = []Thread{{
420			Stacktrace: NewStacktrace(),
421			Crashed:    false,
422			Current:    true,
423		}}
424	}
425
426	return event
427}
428
429func (client *Client) eventFromException(exception error, level Level) *Event {
430	err := exception
431	if err == nil {
432		err = usageError{fmt.Errorf("%s called with nil error", callerFunctionName())}
433	}
434
435	event := NewEvent()
436	event.Level = level
437
438	for i := 0; i < maxErrorDepth && err != nil; i++ {
439		event.Exception = append(event.Exception, Exception{
440			Value:      err.Error(),
441			Type:       reflect.TypeOf(err).String(),
442			Stacktrace: ExtractStacktrace(err),
443		})
444		switch previous := err.(type) {
445		case interface{ Unwrap() error }:
446			err = previous.Unwrap()
447		case interface{ Cause() error }:
448			err = previous.Cause()
449		default:
450			err = nil
451		}
452	}
453
454	// Add a trace of the current stack to the most recent error in a chain if
455	// it doesn't have a stack trace yet.
456	// We only add to the most recent error to avoid duplication and because the
457	// current stack is most likely unrelated to errors deeper in the chain.
458	if event.Exception[0].Stacktrace == nil {
459		event.Exception[0].Stacktrace = NewStacktrace()
460	}
461
462	// event.Exception should be sorted such that the most recent error is last.
463	reverse(event.Exception)
464
465	return event
466}
467
468// reverse reverses the slice a in place.
469func reverse(a []Exception) {
470	for i := len(a)/2 - 1; i >= 0; i-- {
471		opp := len(a) - 1 - i
472		a[i], a[opp] = a[opp], a[i]
473	}
474}
475
476func (client *Client) processEvent(event *Event, hint *EventHint, scope EventModifier) *EventID {
477	if event == nil {
478		err := usageError{fmt.Errorf("%s called with nil event", callerFunctionName())}
479		return client.CaptureException(err, hint, scope)
480	}
481
482	options := client.Options()
483
484	// The default error event sample rate for all SDKs is 1.0 (send all).
485	//
486	// In Go, the zero value (default) for float64 is 0.0, which means that
487	// constructing a client with NewClient(ClientOptions{}), or, equivalently,
488	// initializing the SDK with Init(ClientOptions{}) without an explicit
489	// SampleRate would drop all events.
490	//
491	// To retain the desired default behavior, we exceptionally flip SampleRate
492	// from 0.0 to 1.0 here. Setting the sample rate to 0.0 is not very useful
493	// anyway, and the same end result can be achieved in many other ways like
494	// not initializing the SDK, setting the DSN to the empty string or using an
495	// event processor that always returns nil.
496	//
497	// An alternative API could be such that default options don't need to be
498	// the same as Go's zero values, for example using the Functional Options
499	// pattern. That would either require a breaking change if we want to reuse
500	// the obvious NewClient name, or a new function as an alternative
501	// constructor.
502	if options.SampleRate == 0.0 {
503		options.SampleRate = 1.0
504	}
505
506	// Transactions are sampled by options.TracesSampleRate or
507	// options.TracesSampler when they are started. All other events
508	// (errors, messages) are sampled here.
509	if event.Type != transactionType && !sample(options.SampleRate) {
510		Logger.Println("Event dropped due to SampleRate hit.")
511		return nil
512	}
513
514	if event = client.prepareEvent(event, hint, scope); event == nil {
515		return nil
516	}
517
518	// As per spec, transactions do not go through BeforeSend.
519	if event.Type != transactionType && options.BeforeSend != nil {
520		if hint == nil {
521			hint = &EventHint{}
522		}
523		if event = options.BeforeSend(event, hint); event == nil {
524			Logger.Println("Event dropped due to BeforeSend callback.")
525			return nil
526		}
527	}
528
529	client.Transport.SendEvent(event)
530
531	return &event.EventID
532}
533
534func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventModifier) *Event {
535	if event.EventID == "" {
536		event.EventID = EventID(uuid())
537	}
538
539	if event.Timestamp.IsZero() {
540		event.Timestamp = time.Now()
541	}
542
543	if event.Level == "" {
544		event.Level = LevelInfo
545	}
546
547	if event.ServerName == "" {
548		if client.Options().ServerName != "" {
549			event.ServerName = client.Options().ServerName
550		} else {
551			event.ServerName = hostname
552		}
553	}
554
555	if event.Release == "" && client.Options().Release != "" {
556		event.Release = client.Options().Release
557	}
558
559	if event.Dist == "" && client.Options().Dist != "" {
560		event.Dist = client.Options().Dist
561	}
562
563	if event.Environment == "" && client.Options().Environment != "" {
564		event.Environment = client.Options().Environment
565	}
566
567	event.Platform = "go"
568	event.Sdk = SdkInfo{
569		Name:         "sentry.go",
570		Version:      Version,
571		Integrations: client.listIntegrations(),
572		Packages: []SdkPackage{{
573			Name:    "sentry-go",
574			Version: Version,
575		}},
576	}
577
578	if scope != nil {
579		event = scope.ApplyToEvent(event, hint)
580		if event == nil {
581			return nil
582		}
583	}
584
585	for _, processor := range client.eventProcessors {
586		id := event.EventID
587		event = processor(event, hint)
588		if event == nil {
589			Logger.Printf("Event dropped by one of the Client EventProcessors: %s\n", id)
590			return nil
591		}
592	}
593
594	for _, processor := range globalEventProcessors {
595		id := event.EventID
596		event = processor(event, hint)
597		if event == nil {
598			Logger.Printf("Event dropped by one of the Global EventProcessors: %s\n", id)
599			return nil
600		}
601	}
602
603	return event
604}
605
606func (client Client) listIntegrations() []string {
607	integrations := make([]string, 0, len(client.integrations))
608	for _, integration := range client.integrations {
609		integrations = append(integrations, integration.Name())
610	}
611	sort.Strings(integrations)
612	return integrations
613}
614
615func (client Client) integrationAlreadyInstalled(name string) bool {
616	for _, integration := range client.integrations {
617		if integration.Name() == name {
618			return true
619		}
620	}
621	return false
622}
623
624// sample returns true with the given probability, which must be in the range
625// [0.0, 1.0].
626func sample(probability float64) bool {
627	return rng.Float64() < probability
628}
629