package sentry import ( "bytes" "io" "net/http" "reflect" "sync" "time" ) // Scope holds contextual data for the current scope. // // The scope is an object that can cloned efficiently and stores data that // is locally relevant to an event. For instance the scope will hold recorded // breadcrumbs and similar information. // // The scope can be interacted with in two ways: // // 1. the scope is routinely updated with information by functions such as // `AddBreadcrumb` which will modify the currently top-most scope. // 2. the topmost scope can also be configured through the `ConfigureScope` // method. // // Note that the scope can only be modified but not inspected. // Only the client can use the scope to extract information currently. type Scope struct { mu sync.RWMutex breadcrumbs []*Breadcrumb user User tags map[string]string contexts map[string]interface{} extra map[string]interface{} fingerprint []string level Level transaction string request *http.Request // requestBody holds a reference to the original request.Body. requestBody interface { // Bytes returns bytes from the original body, lazily buffered as the // original body is read. Bytes() []byte // Overflow returns true if the body is larger than the maximum buffer // size. Overflow() bool } eventProcessors []EventProcessor } func NewScope() *Scope { scope := Scope{ breadcrumbs: make([]*Breadcrumb, 0), tags: make(map[string]string), contexts: make(map[string]interface{}), extra: make(map[string]interface{}), fingerprint: make([]string, 0), } return &scope } // AddBreadcrumb adds new breadcrumb to the current scope // and optionally throws the old one if limit is reached. func (scope *Scope) AddBreadcrumb(breadcrumb *Breadcrumb, limit int) { if breadcrumb.Timestamp.IsZero() { breadcrumb.Timestamp = time.Now().UTC() } scope.mu.Lock() defer scope.mu.Unlock() breadcrumbs := append(scope.breadcrumbs, breadcrumb) if len(breadcrumbs) > limit { scope.breadcrumbs = breadcrumbs[1 : limit+1] } else { scope.breadcrumbs = breadcrumbs } } // ClearBreadcrumbs clears all breadcrumbs from the current scope. func (scope *Scope) ClearBreadcrumbs() { scope.mu.Lock() defer scope.mu.Unlock() scope.breadcrumbs = []*Breadcrumb{} } // SetUser sets the user for the current scope. func (scope *Scope) SetUser(user User) { scope.mu.Lock() defer scope.mu.Unlock() scope.user = user } // SetRequest sets the request for the current scope. func (scope *Scope) SetRequest(r *http.Request) { scope.mu.Lock() defer scope.mu.Unlock() scope.request = r if r == nil { return } // Don't buffer request body if we know it is oversized. if r.ContentLength > maxRequestBodyBytes { return } // Don't buffer if there is no body. if r.Body == nil || r.Body == http.NoBody { return } buf := &limitedBuffer{Capacity: maxRequestBodyBytes} r.Body = readCloser{ Reader: io.TeeReader(r.Body, buf), Closer: r.Body, } scope.requestBody = buf } // SetRequestBody sets the request body for the current scope. // // This method should only be called when the body bytes are already available // in memory. Typically, the request body is buffered lazily from the // Request.Body from SetRequest. func (scope *Scope) SetRequestBody(b []byte) { scope.mu.Lock() defer scope.mu.Unlock() capacity := maxRequestBodyBytes overflow := false if len(b) > capacity { overflow = true b = b[:capacity] } scope.requestBody = &limitedBuffer{ Capacity: capacity, Buffer: *bytes.NewBuffer(b), overflow: overflow, } } // maxRequestBodyBytes is the default maximum request body size to send to // Sentry. const maxRequestBodyBytes = 10 * 1024 // A limitedBuffer is like a bytes.Buffer, but limited to store at most Capacity // bytes. Any writes past the capacity are silently discarded, similar to // ioutil.Discard. type limitedBuffer struct { Capacity int bytes.Buffer overflow bool } // Write implements io.Writer. func (b *limitedBuffer) Write(p []byte) (n int, err error) { // Silently ignore writes after overflow. if b.overflow { return len(p), nil } left := b.Capacity - b.Len() if left < 0 { left = 0 } if len(p) > left { b.overflow = true p = p[:left] } return b.Buffer.Write(p) } // Overflow returns true if the limitedBuffer discarded bytes written to it. func (b *limitedBuffer) Overflow() bool { return b.overflow } // readCloser combines an io.Reader and an io.Closer to implement io.ReadCloser. type readCloser struct { io.Reader io.Closer } // SetTag adds a tag to the current scope. func (scope *Scope) SetTag(key, value string) { scope.mu.Lock() defer scope.mu.Unlock() scope.tags[key] = value } // SetTags assigns multiple tags to the current scope. func (scope *Scope) SetTags(tags map[string]string) { scope.mu.Lock() defer scope.mu.Unlock() for k, v := range tags { scope.tags[k] = v } } // RemoveTag removes a tag from the current scope. func (scope *Scope) RemoveTag(key string) { scope.mu.Lock() defer scope.mu.Unlock() delete(scope.tags, key) } // SetContext adds a context to the current scope. func (scope *Scope) SetContext(key string, value interface{}) { scope.mu.Lock() defer scope.mu.Unlock() scope.contexts[key] = value } // SetContexts assigns multiple contexts to the current scope. func (scope *Scope) SetContexts(contexts map[string]interface{}) { scope.mu.Lock() defer scope.mu.Unlock() for k, v := range contexts { scope.contexts[k] = v } } // RemoveContext removes a context from the current scope. func (scope *Scope) RemoveContext(key string) { scope.mu.Lock() defer scope.mu.Unlock() delete(scope.contexts, key) } // SetExtra adds an extra to the current scope. func (scope *Scope) SetExtra(key string, value interface{}) { scope.mu.Lock() defer scope.mu.Unlock() scope.extra[key] = value } // SetExtras assigns multiple extras to the current scope. func (scope *Scope) SetExtras(extra map[string]interface{}) { scope.mu.Lock() defer scope.mu.Unlock() for k, v := range extra { scope.extra[k] = v } } // RemoveExtra removes a extra from the current scope. func (scope *Scope) RemoveExtra(key string) { scope.mu.Lock() defer scope.mu.Unlock() delete(scope.extra, key) } // SetFingerprint sets new fingerprint for the current scope. func (scope *Scope) SetFingerprint(fingerprint []string) { scope.mu.Lock() defer scope.mu.Unlock() scope.fingerprint = fingerprint } // SetLevel sets new level for the current scope. func (scope *Scope) SetLevel(level Level) { scope.mu.Lock() defer scope.mu.Unlock() scope.level = level } // SetTransaction sets new transaction name for the current transaction. func (scope *Scope) SetTransaction(transactionName string) { scope.mu.Lock() defer scope.mu.Unlock() scope.transaction = transactionName } // Clone returns a copy of the current scope with all data copied over. func (scope *Scope) Clone() *Scope { scope.mu.RLock() defer scope.mu.RUnlock() clone := NewScope() clone.user = scope.user clone.breadcrumbs = make([]*Breadcrumb, len(scope.breadcrumbs)) copy(clone.breadcrumbs, scope.breadcrumbs) for key, value := range scope.tags { clone.tags[key] = value } for key, value := range scope.contexts { clone.contexts[key] = value } for key, value := range scope.extra { clone.extra[key] = value } clone.fingerprint = make([]string, len(scope.fingerprint)) copy(clone.fingerprint, scope.fingerprint) clone.level = scope.level clone.transaction = scope.transaction clone.request = scope.request return clone } // Clear removes the data from the current scope. Not safe for concurrent use. func (scope *Scope) Clear() { *scope = *NewScope() } // AddEventProcessor adds an event processor to the current scope. func (scope *Scope) AddEventProcessor(processor EventProcessor) { scope.mu.Lock() defer scope.mu.Unlock() scope.eventProcessors = append(scope.eventProcessors, processor) } // ApplyToEvent takes the data from the current scope and attaches it to the event. func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint) *Event { scope.mu.RLock() defer scope.mu.RUnlock() if len(scope.breadcrumbs) > 0 { if event.Breadcrumbs == nil { event.Breadcrumbs = []*Breadcrumb{} } event.Breadcrumbs = append(event.Breadcrumbs, scope.breadcrumbs...) } if len(scope.tags) > 0 { if event.Tags == nil { event.Tags = make(map[string]string) } for key, value := range scope.tags { event.Tags[key] = value } } if len(scope.contexts) > 0 { if event.Contexts == nil { event.Contexts = make(map[string]interface{}) } for key, value := range scope.contexts { event.Contexts[key] = value } } if len(scope.extra) > 0 { if event.Extra == nil { event.Extra = make(map[string]interface{}) } for key, value := range scope.extra { event.Extra[key] = value } } if (reflect.DeepEqual(event.User, User{})) { event.User = scope.user } if (event.Fingerprint == nil || len(event.Fingerprint) == 0) && len(scope.fingerprint) > 0 { event.Fingerprint = make([]string, len(scope.fingerprint)) copy(event.Fingerprint, scope.fingerprint) } if scope.level != "" { event.Level = scope.level } if scope.transaction != "" { event.Transaction = scope.transaction } if event.Request == nil && scope.request != nil { event.Request = NewRequest(scope.request) // NOTE: The SDK does not attempt to send partial request body data. // // The reason being that Sentry's ingest pipeline and UI are optimized // to show structured data. Additionally, tooling around PII scrubbing // relies on structured data; truncated request bodies would create // invalid payloads that are more prone to leaking PII data. // // Users can still send more data along their events if they want to, // for example using Event.Extra. if scope.requestBody != nil && !scope.requestBody.Overflow() { event.Request.Data = string(scope.requestBody.Bytes()) } } for _, processor := range scope.eventProcessors { id := event.EventID event = processor(event, hint) if event == nil { Logger.Printf("Event dropped by one of the Scope EventProcessors: %s\n", id) return nil } } return event }