package sentry import ( "context" "encoding/json" "fmt" "net" "net/http" "strings" "time" "github.com/getsentry/sentry-go/attribute" "github.com/getsentry/sentry-go/internal/protocol" "github.com/getsentry/sentry-go/internal/ratelimit" ) const eventType = "event" const transactionType = "transaction" const checkInType = "check_in" var logEvent = struct { Type string ContentType string }{ "log", "application/vnd.sentry.items.log+json", } // Level marks the severity of the event. type Level string // Describes the severity of the event. const ( LevelDebug Level = "debug" LevelInfo Level = "info" LevelWarning Level = "warning" LevelError Level = "error" LevelFatal Level = "fatal" ) // SdkInfo contains all metadata about the SDK. type SdkInfo = protocol.SdkInfo type SdkPackage = protocol.SdkPackage // TODO: This type could be more useful, as map of interface{} is too generic // and requires a lot of type assertions in beforeBreadcrumb calls // plus it could just be map[string]interface{} then. // BreadcrumbHint contains information that can be associated with a Breadcrumb. type BreadcrumbHint map[string]interface{} // Breadcrumb specifies an application event that occurred before a Sentry event. // An event may contain one or more breadcrumbs. type Breadcrumb struct { Type string `json:"type,omitempty"` Category string `json:"category,omitempty"` Message string `json:"message,omitempty"` Data map[string]interface{} `json:"data,omitempty"` Level Level `json:"level,omitempty"` Timestamp time.Time `json:"timestamp"` } // TODO: provide constants for known breadcrumb types. // See https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/#breadcrumb-types. // MarshalJSON converts the Breadcrumb struct to JSON. func (b *Breadcrumb) MarshalJSON() ([]byte, error) { // We want to omit time.Time zero values, otherwise the server will try to // interpret dates too far in the past. However, encoding/json doesn't // support the "omitempty" option for struct types. See // https://golang.org/issues/11939. // // We overcome the limitation and achieve what we want by shadowing fields // and a few type tricks. // breadcrumb aliases Breadcrumb to allow calling json.Marshal without an // infinite loop. It preserves all fields while none of the attached // methods. type breadcrumb Breadcrumb if b.Timestamp.IsZero() { return json.Marshal(struct { // Embed all of the fields of Breadcrumb. *breadcrumb // Timestamp shadows the original Timestamp field and is meant to // remain nil, triggering the omitempty behavior. Timestamp json.RawMessage `json:"timestamp,omitempty"` }{breadcrumb: (*breadcrumb)(b)}) } return json.Marshal((*breadcrumb)(b)) } // Logger provides a chaining API for structured logging to Sentry. type Logger interface { // Write implements the io.Writer interface. Currently, the [sentry.Hub] is // context aware, in order to get the correct trace correlation. Using this // might result in incorrect span association on logs. If you need to use // Write it is recommended to create a NewLogger so that the associated context // is passed correctly. Write(p []byte) (n int, err error) // SetAttributes allows attaching parameters to the logger using the attribute API. // These attributes will be included in all subsequent log entries. SetAttributes(...attribute.Builder) // Trace defines the [sentry.LogLevel] for the log entry. Trace() LogEntry // Debug defines the [sentry.LogLevel] for the log entry. Debug() LogEntry // Info defines the [sentry.LogLevel] for the log entry. Info() LogEntry // Warn defines the [sentry.LogLevel] for the log entry. Warn() LogEntry // Error defines the [sentry.LogLevel] for the log entry. Error() LogEntry // Fatal defines the [sentry.LogLevel] for the log entry. Fatal() LogEntry // Panic defines the [sentry.LogLevel] for the log entry. Panic() LogEntry // GetCtx returns the [context.Context] set on the logger. GetCtx() context.Context } // LogEntry defines the interface for a log entry that supports chaining attributes. type LogEntry interface { // WithCtx creates a new LogEntry with the specified context without overwriting the previous one. WithCtx(ctx context.Context) LogEntry // String adds a string attribute to the LogEntry. String(key, value string) LogEntry // Int adds an int attribute to the LogEntry. Int(key string, value int) LogEntry // Int64 adds an int64 attribute to the LogEntry. Int64(key string, value int64) LogEntry // Float64 adds a float64 attribute to the LogEntry. Float64(key string, value float64) LogEntry // Bool adds a bool attribute to the LogEntry. Bool(key string, value bool) LogEntry // Emit emits the LogEntry with the provided arguments. Emit(args ...interface{}) // Emitf emits the LogEntry using a format string and arguments. Emitf(format string, args ...interface{}) } // Attachment allows associating files with your events to aid in investigation. // An event may contain one or more attachments. type Attachment struct { Filename string ContentType string Payload []byte } // User describes the user associated with an Event. If this is used, at least // an ID or an IP address should be provided. type User struct { ID string `json:"id,omitempty"` Email string `json:"email,omitempty"` IPAddress string `json:"ip_address,omitempty"` Username string `json:"username,omitempty"` Name string `json:"name,omitempty"` Data map[string]string `json:"data,omitempty"` } func (u User) IsEmpty() bool { if u.ID != "" { return false } if u.Email != "" { return false } if u.IPAddress != "" { return false } if u.Username != "" { return false } if u.Name != "" { return false } if len(u.Data) > 0 { return false } return true } // Request contains information on a HTTP request related to the event. type Request struct { URL string `json:"url,omitempty"` Method string `json:"method,omitempty"` Data string `json:"data,omitempty"` QueryString string `json:"query_string,omitempty"` Cookies string `json:"cookies,omitempty"` Headers map[string]string `json:"headers,omitempty"` Env map[string]string `json:"env,omitempty"` } var sensitiveHeaders = map[string]struct{}{ "_csrf": {}, "_csrf_token": {}, "_session": {}, "_xsrf": {}, "Api-Key": {}, "Apikey": {}, "Auth": {}, "Authorization": {}, "Cookie": {}, "Credentials": {}, "Csrf": {}, "Csrf-Token": {}, "Csrftoken": {}, "Ip-Address": {}, "Passwd": {}, "Password": {}, "Private-Key": {}, "Privatekey": {}, "Proxy-Authorization": {}, "Remote-Addr": {}, "Secret": {}, "Session": {}, "Sessionid": {}, "Token": {}, "User-Session": {}, "X-Api-Key": {}, "X-Csrftoken": {}, "X-Forwarded-For": {}, "X-Real-Ip": {}, "XSRF-TOKEN": {}, } // NewRequest returns a new Sentry Request from the given http.Request. // // NewRequest avoids operations that depend on network access. In particular, it // does not read r.Body. func NewRequest(r *http.Request) *Request { prot := protocol.SchemeHTTP if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" { prot = protocol.SchemeHTTPS } url := fmt.Sprintf("%s://%s%s", prot, r.Host, r.URL.Path) var cookies string var env map[string]string headers := map[string]string{} if client := CurrentHub().Client(); client != nil && client.options.SendDefaultPII { // We read only the first Cookie header because of the specification: // https://tools.ietf.org/html/rfc6265#section-5.4 // When the user agent generates an HTTP request, the user agent MUST NOT // attach more than one Cookie header field. cookies = r.Header.Get("Cookie") headers = make(map[string]string, len(r.Header)) for k, v := range r.Header { headers[k] = strings.Join(v, ",") } if addr, port, err := net.SplitHostPort(r.RemoteAddr); err == nil { env = map[string]string{"REMOTE_ADDR": addr, "REMOTE_PORT": port} } } else { for k, v := range r.Header { if _, ok := sensitiveHeaders[k]; !ok { headers[k] = strings.Join(v, ",") } } } headers["Host"] = r.Host return &Request{ URL: url, Method: r.Method, QueryString: r.URL.RawQuery, Cookies: cookies, Headers: headers, Env: env, } } // Mechanism is the mechanism by which an exception was generated and handled. type Mechanism struct { Type string `json:"type"` Description string `json:"description,omitempty"` HelpLink string `json:"help_link,omitempty"` Source string `json:"source,omitempty"` Handled *bool `json:"handled,omitempty"` ParentID *int `json:"parent_id,omitempty"` ExceptionID int `json:"exception_id"` IsExceptionGroup bool `json:"is_exception_group,omitempty"` Data map[string]any `json:"data,omitempty"` } // SetUnhandled indicates that the exception is an unhandled exception, i.e. // from a panic. func (m *Mechanism) SetUnhandled() { m.Handled = Pointer(false) } // Exception specifies an error that occurred. type Exception struct { Type string `json:"type,omitempty"` // used as the main issue title Value string `json:"value,omitempty"` // used as the main issue subtitle Module string `json:"module,omitempty"` ThreadID uint64 `json:"thread_id,omitempty"` Stacktrace *Stacktrace `json:"stacktrace,omitempty"` Mechanism *Mechanism `json:"mechanism,omitempty"` } // SDKMetaData is a struct to stash data which is needed at some point in the SDK's event processing pipeline // but which shouldn't get send to Sentry. type SDKMetaData struct { dsc DynamicSamplingContext } // Contains information about how the name of the transaction was determined. type TransactionInfo struct { Source TransactionSource `json:"source,omitempty"` } // The DebugMeta interface is not used in Golang apps, but may be populated // when proxying Events from other platforms, like iOS, Android, and the // Web. (See: https://develop.sentry.dev/sdk/event-payloads/debugmeta/ ). type DebugMeta struct { SdkInfo *DebugMetaSdkInfo `json:"sdk_info,omitempty"` Images []DebugMetaImage `json:"images,omitempty"` } type DebugMetaSdkInfo struct { SdkName string `json:"sdk_name,omitempty"` VersionMajor int `json:"version_major,omitempty"` VersionMinor int `json:"version_minor,omitempty"` VersionPatchlevel int `json:"version_patchlevel,omitempty"` } type DebugMetaImage struct { Type string `json:"type,omitempty"` // all ImageAddr string `json:"image_addr,omitempty"` // macho,elf,pe ImageSize int `json:"image_size,omitempty"` // macho,elf,pe DebugID string `json:"debug_id,omitempty"` // macho,elf,pe,wasm,sourcemap DebugFile string `json:"debug_file,omitempty"` // macho,elf,pe,wasm CodeID string `json:"code_id,omitempty"` // macho,elf,pe,wasm CodeFile string `json:"code_file,omitempty"` // macho,elf,pe,wasm,sourcemap ImageVmaddr string `json:"image_vmaddr,omitempty"` // macho,elf,pe Arch string `json:"arch,omitempty"` // macho,elf,pe UUID string `json:"uuid,omitempty"` // proguard } // EventID is a hexadecimal string representing a unique uuid4 for an Event. // An EventID must be 32 characters long, lowercase and not have any dashes. type EventID string type Context = map[string]interface{} // Event is the fundamental data structure that is sent to Sentry. type Event struct { Breadcrumbs []*Breadcrumb `json:"breadcrumbs,omitempty"` Contexts map[string]Context `json:"contexts,omitempty"` Dist string `json:"dist,omitempty"` Environment string `json:"environment,omitempty"` EventID EventID `json:"event_id,omitempty"` Extra map[string]interface{} `json:"extra,omitempty"` Fingerprint []string `json:"fingerprint,omitempty"` Level Level `json:"level,omitempty"` Message string `json:"message,omitempty"` Platform string `json:"platform,omitempty"` Release string `json:"release,omitempty"` Sdk SdkInfo `json:"sdk,omitempty"` ServerName string `json:"server_name,omitempty"` Threads []Thread `json:"threads,omitempty"` Tags map[string]string `json:"tags,omitempty"` Timestamp time.Time `json:"timestamp"` Transaction string `json:"transaction,omitempty"` User User `json:"user,omitempty"` Logger string `json:"logger,omitempty"` Modules map[string]string `json:"modules,omitempty"` Request *Request `json:"request,omitempty"` Exception []Exception `json:"exception,omitempty"` DebugMeta *DebugMeta `json:"debug_meta,omitempty"` Attachments []*Attachment `json:"-"` // The fields below are only relevant for transactions. Type string `json:"type,omitempty"` StartTime time.Time `json:"start_timestamp"` Spans []*Span `json:"spans,omitempty"` TransactionInfo *TransactionInfo `json:"transaction_info,omitempty"` // The fields below are only relevant for crons/check ins CheckIn *CheckIn `json:"check_in,omitempty"` MonitorConfig *MonitorConfig `json:"monitor_config,omitempty"` // The fields below are only relevant for logs Logs []Log `json:"items,omitempty"` // The fields below are not part of the final JSON payload. sdkMetaData SDKMetaData } // SetException appends the unwrapped errors to the event's exception list. // // maxErrorDepth is the maximum depth of the error chain we will look // into while unwrapping the errors. If maxErrorDepth is -1, we will // unwrap all errors in the chain. func (e *Event) SetException(exception error, maxErrorDepth int) { if exception == nil { return } exceptions := convertErrorToExceptions(exception, maxErrorDepth) if len(exceptions) == 0 { return } e.Exception = exceptions } // ToEnvelope converts the Event to a Sentry envelope. // This includes the event data and any attachments as separate envelope items. func (e *Event) ToEnvelope(dsn *protocol.Dsn) (*protocol.Envelope, error) { return e.ToEnvelopeWithTime(dsn, time.Now()) } // ToEnvelopeWithTime converts the Event to a Sentry envelope with a specific sentAt time. // This is primarily useful for testing with predictable timestamps. func (e *Event) ToEnvelopeWithTime(dsn *protocol.Dsn, sentAt time.Time) (*protocol.Envelope, error) { // Create envelope header with trace context trace := make(map[string]string) if dsc := e.sdkMetaData.dsc; dsc.HasEntries() { for k, v := range dsc.Entries { trace[k] = v } } header := &protocol.EnvelopeHeader{ EventID: string(e.EventID), SentAt: sentAt, Trace: trace, } if dsn != nil { header.Dsn = dsn.String() } header.Sdk = &e.Sdk envelope := protocol.NewEnvelope(header) eventBody, err := json.Marshal(e) if err != nil { // Try fallback: remove problematic fields and retry e.Breadcrumbs = nil e.Contexts = nil e.Extra = map[string]interface{}{ "info": fmt.Sprintf("Could not encode original event as JSON. "+ "Succeeded by removing Breadcrumbs, Contexts and Extra. "+ "Please verify the data you attach to the scope. "+ "Error: %s", err), } eventBody, err = json.Marshal(e) if err != nil { return nil, fmt.Errorf("event could not be marshaled even with fallback: %w", err) } DebugLogger.Printf("Event marshaling succeeded with fallback after removing problematic fields") } var mainItem *protocol.EnvelopeItem switch e.Type { case transactionType: mainItem = protocol.NewEnvelopeItem(protocol.EnvelopeItemTypeTransaction, eventBody) case checkInType: mainItem = protocol.NewEnvelopeItem(protocol.EnvelopeItemTypeCheckIn, eventBody) case logEvent.Type: mainItem = protocol.NewLogItem(len(e.Logs), eventBody) default: mainItem = protocol.NewEnvelopeItem(protocol.EnvelopeItemTypeEvent, eventBody) } envelope.AddItem(mainItem) for _, attachment := range e.Attachments { attachmentItem := protocol.NewAttachmentItem(attachment.Filename, attachment.ContentType, attachment.Payload) envelope.AddItem(attachmentItem) } return envelope, nil } // ToEnvelopeItem converts the Event to a Sentry envelope item. func (e *Event) ToEnvelopeItem() (*protocol.EnvelopeItem, error) { eventBody, err := json.Marshal(e) if err != nil { // Try fallback: remove problematic fields and retry e.Breadcrumbs = nil e.Contexts = nil e.Extra = map[string]interface{}{ "info": fmt.Sprintf("Could not encode original event as JSON. "+ "Succeeded by removing Breadcrumbs, Contexts and Extra. "+ "Please verify the data you attach to the scope. "+ "Error: %s", err), } eventBody, err = json.Marshal(e) if err != nil { return nil, fmt.Errorf("event could not be marshaled even with fallback: %w", err) } DebugLogger.Printf("Event marshaling succeeded with fallback after removing problematic fields") } // TODO: all event types should be abstracted to implement EnvelopeItemConvertible and convert themselves. var item *protocol.EnvelopeItem switch e.Type { case transactionType: item = protocol.NewEnvelopeItem(protocol.EnvelopeItemTypeTransaction, eventBody) case checkInType: item = protocol.NewEnvelopeItem(protocol.EnvelopeItemTypeCheckIn, eventBody) case logEvent.Type: item = protocol.NewLogItem(len(e.Logs), eventBody) default: item = protocol.NewEnvelopeItem(protocol.EnvelopeItemTypeEvent, eventBody) } return item, nil } // GetCategory returns the rate limit category for this event. func (e *Event) GetCategory() ratelimit.Category { return e.toCategory() } // GetEventID returns the event ID. func (e *Event) GetEventID() string { return string(e.EventID) } // GetSdkInfo returns SDK information for the envelope header. func (e *Event) GetSdkInfo() *protocol.SdkInfo { return &e.Sdk } // GetDynamicSamplingContext returns trace context for the envelope header. func (e *Event) GetDynamicSamplingContext() map[string]string { trace := make(map[string]string) if dsc := e.sdkMetaData.dsc; dsc.HasEntries() { for k, v := range dsc.Entries { trace[k] = v } } return trace } // TODO: Event.Contexts map[string]interface{} => map[string]EventContext, // to prevent accidentally storing T when we mean *T. // For example, the TraceContext must be stored as *TraceContext to pick up the // MarshalJSON method (and avoid copying). // type EventContext interface{ EventContext() } // MarshalJSON converts the Event struct to JSON. func (e *Event) MarshalJSON() ([]byte, error) { // We want to omit time.Time zero values, otherwise the server will try to // interpret dates too far in the past. However, encoding/json doesn't // support the "omitempty" option for struct types. See // https://golang.org/issues/11939. // // We overcome the limitation and achieve what we want by shadowing fields // and a few type tricks. if e.Type == transactionType { return e.transactionMarshalJSON() } if e.Type == checkInType { return e.checkInMarshalJSON() } return e.defaultMarshalJSON() } func (e *Event) defaultMarshalJSON() ([]byte, error) { // event aliases Event to allow calling json.Marshal without an infinite // loop. It preserves all fields while none of the attached methods. type event Event // errorEvent is like Event with shadowed fields for customizing JSON // marshaling. type errorEvent struct { *event // Timestamp shadows the original Timestamp field. It allows us to // include the timestamp when non-zero and omit it otherwise. Timestamp json.RawMessage `json:"timestamp,omitempty"` // The fields below are not part of error events and only make sense to // be sent for transactions. They shadow the respective fields in Event // and are meant to remain nil, triggering the omitempty behavior. Type json.RawMessage `json:"type,omitempty"` StartTime json.RawMessage `json:"start_timestamp,omitempty"` Spans json.RawMessage `json:"spans,omitempty"` TransactionInfo json.RawMessage `json:"transaction_info,omitempty"` } x := errorEvent{event: (*event)(e)} if !e.Timestamp.IsZero() { b, err := e.Timestamp.MarshalJSON() if err != nil { return nil, err } x.Timestamp = b } return json.Marshal(x) } func (e *Event) transactionMarshalJSON() ([]byte, error) { // event aliases Event to allow calling json.Marshal without an infinite // loop. It preserves all fields while none of the attached methods. type event Event // transactionEvent is like Event with shadowed fields for customizing JSON // marshaling. type transactionEvent struct { *event // The fields below shadow the respective fields in Event. They allow us // to include timestamps when non-zero and omit them otherwise. StartTime json.RawMessage `json:"start_timestamp,omitempty"` Timestamp json.RawMessage `json:"timestamp,omitempty"` } x := transactionEvent{event: (*event)(e)} if !e.Timestamp.IsZero() { b, err := e.Timestamp.MarshalJSON() if err != nil { return nil, err } x.Timestamp = b } if !e.StartTime.IsZero() { b, err := e.StartTime.MarshalJSON() if err != nil { return nil, err } x.StartTime = b } return json.Marshal(x) } func (e *Event) checkInMarshalJSON() ([]byte, error) { checkIn := serializedCheckIn{ CheckInID: string(e.CheckIn.ID), MonitorSlug: e.CheckIn.MonitorSlug, Status: e.CheckIn.Status, Duration: e.CheckIn.Duration.Seconds(), Release: e.Release, Environment: e.Environment, MonitorConfig: nil, } if e.MonitorConfig != nil { checkIn.MonitorConfig = &MonitorConfig{ Schedule: e.MonitorConfig.Schedule, CheckInMargin: e.MonitorConfig.CheckInMargin, MaxRuntime: e.MonitorConfig.MaxRuntime, Timezone: e.MonitorConfig.Timezone, FailureIssueThreshold: e.MonitorConfig.FailureIssueThreshold, RecoveryThreshold: e.MonitorConfig.RecoveryThreshold, } } return json.Marshal(checkIn) } func (e *Event) toCategory() ratelimit.Category { switch e.Type { case "": return ratelimit.CategoryError case transactionType: return ratelimit.CategoryTransaction case logEvent.Type: return ratelimit.CategoryLog case checkInType: return ratelimit.CategoryMonitor default: return ratelimit.CategoryUnknown } } // NewEvent creates a new Event. func NewEvent() *Event { return &Event{ Contexts: make(map[string]Context), Extra: make(map[string]interface{}), Tags: make(map[string]string), Modules: make(map[string]string), } } // Thread specifies threads that were running at the time of an event. type Thread struct { ID string `json:"id,omitempty"` Name string `json:"name,omitempty"` Stacktrace *Stacktrace `json:"stacktrace,omitempty"` Crashed bool `json:"crashed,omitempty"` Current bool `json:"current,omitempty"` } // EventHint contains information that can be associated with an Event. type EventHint struct { Data interface{} EventID string OriginalException error RecoveredException interface{} Context context.Context Request *http.Request Response *http.Response } type Log struct { Timestamp time.Time `json:"timestamp,omitempty"` TraceID TraceID `json:"trace_id,omitempty"` Level LogLevel `json:"level"` Severity int `json:"severity_number,omitempty"` Body string `json:"body,omitempty"` Attributes map[string]Attribute `json:"attributes,omitempty"` } // ToEnvelopeItem converts the Log to a Sentry envelope item for batching. func (l *Log) ToEnvelopeItem() (*protocol.EnvelopeItem, error) { type logJSON struct { Timestamp *float64 `json:"timestamp,omitempty"` TraceID string `json:"trace_id,omitempty"` Level string `json:"level"` Severity int `json:"severity_number,omitempty"` Body string `json:"body,omitempty"` Attributes map[string]protocol.LogAttribute `json:"attributes,omitempty"` } // Convert time.Time to seconds float if set var ts *float64 if !l.Timestamp.IsZero() { sec := float64(l.Timestamp.UnixNano()) / 1e9 ts = &sec } attrs := make(map[string]protocol.LogAttribute, len(l.Attributes)) for k, v := range l.Attributes { attrs[k] = protocol.LogAttribute{Value: v.Value, Type: string(v.Type)} } logData, err := json.Marshal(logJSON{ Timestamp: ts, TraceID: l.TraceID.String(), Level: string(l.Level), Severity: l.Severity, Body: l.Body, Attributes: attrs, }) if err != nil { return nil, err } return &protocol.EnvelopeItem{ Header: &protocol.EnvelopeItemHeader{ Type: protocol.EnvelopeItemTypeLog, }, Payload: logData, }, nil } // GetCategory returns the rate limit category for logs. func (l *Log) GetCategory() ratelimit.Category { return ratelimit.CategoryLog } // GetEventID returns empty string (event ID set when batching). func (l *Log) GetEventID() string { return "" } // GetSdkInfo returns nil (SDK info set when batching). func (l *Log) GetSdkInfo() *protocol.SdkInfo { return nil } // GetDynamicSamplingContext returns nil (trace context set when batching). func (l *Log) GetDynamicSamplingContext() map[string]string { return nil } type AttrType string const ( AttributeInvalid AttrType = "" AttributeBool AttrType = "boolean" AttributeInt AttrType = "integer" AttributeFloat AttrType = "double" AttributeString AttrType = "string" ) type Attribute struct { Value any `json:"value"` Type AttrType `json:"type"` }