package sentry import ( "context" "encoding/json" "fmt" "net" "net/http" "strings" "time" ) // Protocol Docs (kinda) // https://github.com/getsentry/rust-sentry-types/blob/master/src/protocol/v7.rs // transactionType is the type of a transaction event. const transactionType = "transaction" // 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" ) func getSensitiveHeaders() map[string]bool { return map[string]bool{ "Authorization": true, "Cookie": true, "X-Forwarded-For": true, "X-Real-Ip": true, } } // SdkInfo contains all metadata about about the SDK being used. type SdkInfo struct { Name string `json:"name,omitempty"` Version string `json:"version,omitempty"` Integrations []string `json:"integrations,omitempty"` Packages []SdkPackage `json:"packages,omitempty"` } // SdkPackage describes a package that was installed. type SdkPackage struct { Name string `json:"name,omitempty"` Version string `json:"version,omitempty"` } // 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)) } // 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"` Segment string `json:"segment,omitempty"` Data map[string]string `json:"data,omitempty"` } func (u User) IsEmpty() bool { if len(u.ID) > 0 { return false } if len(u.Email) > 0 { return false } if len(u.IPAddress) > 0 { return false } if len(u.Username) > 0 { return false } if len(u.Name) > 0 { return false } if len(u.Segment) > 0 { 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"` } // 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 { protocol := schemeHTTP if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" { protocol = schemeHTTPS } url := fmt.Sprintf("%s://%s%s", protocol, r.Host, r.URL.Path) var cookies string var env map[string]string headers := map[string]string{} if client := CurrentHub().Client(); client != nil { if 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") 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 { sensitiveHeaders := getSensitiveHeaders() 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, } } // 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 string `json:"thread_id,omitempty"` Stacktrace *Stacktrace `json:"stacktrace,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"` } // 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"` // 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 not part of the final JSON payload. sdkMetaData SDKMetaData } // 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() } 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) } // NewEvent creates a new Event. func NewEvent() *Event { event := Event{ Contexts: make(map[string]Context), Extra: make(map[string]interface{}), Tags: make(map[string]string), Modules: make(map[string]string), } return &event } // 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 }