package bugsnag import ( "fmt" "reflect" "strings" ) // MetaData is added to the Bugsnag dashboard in tabs. Each tab is // a map of strings -> values. You can pass MetaData to Notify, Recover // and AutoNotify as rawData. type MetaData map[string]map[string]interface{} // Update the meta-data with more information. Tabs are merged together such // that unique keys from both sides are preserved, and duplicate keys end up // with the provided values. func (meta MetaData) Update(other MetaData) { for name, tab := range other { if meta[name] == nil { meta[name] = make(map[string]interface{}) } for key, value := range tab { meta[name][key] = value } } } // Add creates a tab of Bugsnag meta-data. // If the tab doesn't yet exist it will be created. // If the key already exists, it will be overwritten. func (meta MetaData) Add(tab string, key string, value interface{}) { if meta[tab] == nil { meta[tab] = make(map[string]interface{}) } meta[tab][key] = value } // AddStruct creates a tab of Bugsnag meta-data. // The struct will be converted to an Object using the // reflect library so any private fields will not be exported. // As a safety measure, if you pass a non-struct the value will be // sent to Bugsnag under the "Extra data" tab. func (meta MetaData) AddStruct(tab string, obj interface{}) { val := sanitizer{}.Sanitize(obj) content, ok := val.(map[string]interface{}) if ok { meta[tab] = content } else { // Wasn't a struct meta.Add("Extra data", tab, obj) } } // Remove any values from meta-data that have keys matching the filters, // and any that are recursive data-structures func (meta MetaData) sanitize(filters []string) interface{} { return sanitizer{ Filters: filters, Seen: make([]interface{}, 0), }.Sanitize(meta) } // The sanitizer is used to remove filtered params and recursion from meta-data. type sanitizer struct { Filters []string Seen []interface{} } func (s sanitizer) Sanitize(data interface{}) interface{} { for _, s := range s.Seen { // TODO: we don't need deep equal here, just type-ignoring equality if reflect.DeepEqual(data, s) { return "[RECURSION]" } } // Sanitizers are passed by value, so we can modify s and it only affects // s.Seen for nested calls. s.Seen = append(s.Seen, data) t := reflect.TypeOf(data) v := reflect.ValueOf(data) switch t.Kind() { case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, reflect.Float32, reflect.Float64: return data case reflect.String: return data case reflect.Interface, reflect.Ptr: return s.Sanitize(v.Elem().Interface()) case reflect.Array, reflect.Slice: ret := make([]interface{}, v.Len()) for i := 0; i < v.Len(); i++ { ret[i] = s.Sanitize(v.Index(i).Interface()) } return ret case reflect.Map: return s.sanitizeMap(v) case reflect.Struct: return s.sanitizeStruct(v, t) // Things JSON can't serialize: // case t.Chan, t.Func, reflect.Complex64, reflect.Complex128, reflect.UnsafePointer: default: return "[" + t.String() + "]" } } func (s sanitizer) sanitizeMap(v reflect.Value) interface{} { ret := make(map[string]interface{}) for _, key := range v.MapKeys() { val := s.Sanitize(v.MapIndex(key).Interface()) newKey := fmt.Sprintf("%v", key.Interface()) if s.shouldRedact(newKey) { val = "[REDACTED]" } ret[newKey] = val } return ret } func (s sanitizer) sanitizeStruct(v reflect.Value, t reflect.Type) interface{} { ret := make(map[string]interface{}) for i := 0; i < v.NumField(); i++ { val := v.Field(i) // Don't export private fields if !val.CanInterface() { continue } name := t.Field(i).Name var opts tagOptions // Parse JSON tags. Supports name and "omitempty" if jsonTag := t.Field(i).Tag.Get("json"); len(jsonTag) != 0 { name, opts = parseTag(jsonTag) } if s.shouldRedact(name) { ret[name] = "[REDACTED]" } else { sanitized := s.Sanitize(val.Interface()) if str, ok := sanitized.(string); ok { if !(opts.Contains("omitempty") && len(str) == 0) { ret[name] = str } } else { ret[name] = sanitized } } } return ret } func (s sanitizer) shouldRedact(key string) bool { for _, filter := range s.Filters { if strings.Contains(strings.ToLower(filter), strings.ToLower(key)) { return true } } return false }