@@ -28,87 +28,222 @@ import (
2828// BuildSentryReport builds the components of a sentry report. This
2929// can be used instead of ReportError() below to use additional custom
3030// conditions in the reporting or add additional reporting tags.
31+ //
32+ // The Sentry Event is populated for maximal utility when exploited in
33+ // the Sentry.io web interface and database.
34+ //
35+ // A Sentry report is displayed visually in the Sentry UI as follows:
36+ //
37+ ////////////////
38+ // Title: (1) some prefix in bold (2) one line for a stack trace
39+ // (3) a single-line subtitle
40+ //
41+ // (4) the tags, as a tag soup (concatenated in a single paragrah,
42+ // unsorted)
43+ //
44+ // (5) a "message"
45+ //
46+ // (6) zero or more "exceptions", each composed of:
47+ // (7) a bold title
48+ // (8) some freeform text
49+ // (9) a stack trace
50+ //
51+ // (10) metadata fields: environment, arch, etc
52+ //
53+ // (11) "Additional data" fields
54+ //
55+ // (12) SDK version
56+ /////////////////
57+ //
58+ // These visual items map to the Sentry Event object as follows:
59+ //
60+ // (1) the Type field of the 1st Exception object, if any
61+ // otherwise the Message field
62+ // (2) the topmost entry from the Stacktrace field of the 1st Exception object, if any
63+ // (3) the Value field of the 1st Exception object, if any, unwrapped as a single line
64+ // (4) the Tags field
65+ // (5) the Message field
66+ // (7) the Type field (same as (1) for 1st execption)
67+ // (8) the Value field (same as (3) for 1st exception)
68+ // (9) the Stacktrace field (input to (2) on 1st exception)
69+ // (10) the other fields on the Event object
70+ // (11) the Extra field
71+ //
72+ // (Note how the top-level title fields (1) (3) are unrelated to the
73+ // Message field in the event, which is surprising!)
74+ //
75+ // Given this mapping, an error object is decomposed as follows:
76+ //
77+ // (1)/(7): <filename>:<lineno> (<functionname>)
78+ // (3)/(8): <error type>: <first safe detail line, if any>
79+ // (4): not populated in this function, caller is to manage this
80+ // (5): detailed structure of the entire error object, with references to "additional data"
81+ // and additional "exception" objects
82+ // (9): generated from innermost stack trace
83+ // (6): every exception object after the 1st reports additional stack trace contexts
84+ // (11): "additional data" populated from safe detail payloads
85+ //
86+ // If there is no stack trace in the error, a synthetic Exception
87+ // object is still produced to provide visual detail in the Sentry UI.
88+ //
89+ // Note that if a layer in the error has both a stack trace (ie
90+ // provides the `StackTrace()` interface) and also safe details
91+ // (`SafeDetails()`) other than the stack trace, only the stack trace
92+ // is included in the Sentry report. This does not affect error types
93+ // provided by the library, but could impact error types defined by
94+ // 3rd parties. This limitation may be lifted in a later version.
95+ //
3196func BuildSentryReport (err error ) (event * sentry.Event , extraDetails map [string ]interface {}) {
3297 if err == nil {
3398 // No error: do nothing.
3499 return
35100 }
36101
102+ // First step is to collect the details.
37103 var stacks []* withstack.ReportableStackTrace
38104 var details []errbase.SafeDetailPayload
39- // Peel the error.
40105 for c := err ; c != nil ; c = errbase .UnwrapOnce (c ) {
41106 st := withstack .GetReportableStackTrace (c )
42107 stacks = append (stacks , st )
43108
44109 sd := errbase .GetSafeDetails (c )
45110 details = append (details , sd )
46111 }
112+ module := string (domains .GetDomain (err ))
113+
114+ // For the summary, we collect the innermost source context.
115+ // file, line, fn, hasOneLineSource := withstack.GetOneLineSource(err)
116+
117+ // longMsgBuf will become the Message field, which contains the full
118+ // structure of the error with cross-references to "Exception" and
119+ // "Additional data" fields.
120+ var longMsgBuf strings.Builder
121+
122+ // sep is used to separate the entries in the longMsgBuf / Message
123+ // payload.
124+ sep := ""
47125
48- // A report can contain at most one "message", any number of
49- // "exceptions", and arbitrarily many "extra" fields.
50- //
51- // So we populate the event as follow:
52- // - the "message" will contain the type of the first error
53- // - the "exceptions" will contain the details with
54- // populated encoded exceptions field.
55- // - the "extra" will contain all the encoded stack traces
56- // or safe detail arrays.
57-
58- var firstError * string
59- var exceptions []* withstack.ReportableStackTrace
126+ // extras will become the per-layer "Additional data" fields.
60127 extras := make (map [string ]interface {})
61- var longMsgBuf bytes.Buffer
62- var typesBuf bytes.Buffer
63128
129+ // extraNum counts the number of "Additional data" payloads and is
130+ // used to generate the cross-reference counters in the Message
131+ // payload.
64132 extraNum := 1
65- sep := ""
66- for i := len (details ) - 1 ; i >= 0 ; i -- {
67- longMsgBuf .WriteString (sep )
68- sep = "\n "
69133
70- // Collect the type name.
71- tn := details [i ].OriginalTypeName
134+ // typesBuf will become the payload for the "error types" Additional
135+ // data field. It explains the Go types of the layers in the error
136+ // object.
137+ var typesBuf strings.Builder
138+
139+ // exceptions accumulates the Exception payloads.
140+ var exceptions []sentry.Exception
141+
142+ // leafErrorType is the type name of the leaf error.
143+ // This is used as fallback when no Exception payload is generated.
144+ var leafErrorType string
145+
146+ // firstDetailLine is the first detail string encountered.
147+ // This is added as decoration to the first Exception
148+ // payload (either from the error object or synthetic)
149+ // so as to populate the Sentry report title.
150+ var firstDetailLine string
151+
152+ // Iterate from the last (innermost) to first (outermost) error
153+ // layer. We iterate in this order because we want to describe the
154+ // error from innermost to outermost layer in longMsgBuf and
155+ // typesBuf.
156+ for i := len (details ) - 1 ; i >= 0 ; i -- {
157+ // Collect the type name for this layer of error wrapping, towards
158+ // the "error types" additional data field.
159+ fullTypeName := details [i ].OriginalTypeName
72160 mark := details [i ].ErrorTypeMark
73161 fm := "*"
74- if tn != mark .FamilyName {
162+ if fullTypeName != mark .FamilyName {
163+ // fullTypeName can be different from the family when an error type has
164+ // been renamed or moved.
75165 fm = mark .FamilyName
76166 }
77- fmt .Fprintf (& typesBuf , "%s (%s::%s)\n " , tn , fm , mark .Extension )
167+ fmt .Fprintf (& typesBuf , "%s (%s::%s)\n " , fullTypeName , fm , mark .Extension )
168+ shortTypename := lastPathComponent (fullTypeName )
169+ if i == len (details )- 1 {
170+ leafErrorType = shortTypename
171+ }
78172
79- // Compose the message for this layer. The message consists of:
173+ // Compose the Message line for this layer.
174+ //
175+ // The message line consists of:
80176 // - optionally, a file/line reference, if a stack trace was available.
81177 // - the error/wrapper type name, with file prefix removed.
82178 // - optionally, the first line of the first detail string, if one is available.
83179 // - optionally, references to stack trace / details.
180+
181+ // If not at the first layer, separate from the previous layer
182+ // with a newline character.
183+ longMsgBuf .WriteString (sep )
184+ sep = "\n "
185+ // Add a file:lineno prefix, if there's a stack trace entry with
186+ // that info.
187+ var file , fn string
188+ var lineno int
84189 if stacks [i ] != nil && len (stacks [i ].Frames ) > 0 {
85190 f := stacks [i ].Frames [len (stacks [i ].Frames )- 1 ]
86- fn := f .Filename
87- if j := strings .LastIndexByte (fn , '/' ); j >= 0 {
88- fn = fn [j + 1 :]
89- }
90- fmt .Fprintf (& longMsgBuf , "%s:%d: " , fn , f .Lineno )
191+ file = lastPathComponent (f .Filename )
192+ fn = f .Function
193+ lineno = f .Lineno
194+ fmt .Fprintf (& longMsgBuf , "%s:%d: " , file , f .Lineno )
91195 }
196+ longMsgBuf .WriteString (shortTypename )
92197
93- longMsgBuf .WriteString (simpleErrType (tn ))
198+ // Now decide what kind of payload we want to add to the Event
199+ // object.
94200
201+ // genExtra will remember whether we are adding an
202+ // additional payload or not.
95203 var genExtra bool
96204
97205 // Is there a stack trace?
98206 if st := stacks [i ]; st != nil {
99- // Yes: generate the extra and list it on the line.
100- stKey := fmt .Sprintf ("%d: stacktrace" , extraNum )
101- extras [stKey ] = PrintStackTrace (st )
102- fmt .Fprintf (& longMsgBuf , " (%d)" , extraNum )
103- extraNum ++
207+ var excType strings.Builder
208+ if file != "" {
209+ fmt .Fprintf (& excType , "%s:%d " , file , lineno )
210+ }
211+ if fn != "" {
212+ fmt .Fprintf (& excType , "(%s)" , fn )
213+ }
214+ if excType .Len () == 0 {
215+ excType .WriteString ("<unknown error>" )
216+ }
217+ exc := sentry.Exception {
218+ Module : module ,
219+ Stacktrace : st ,
220+ Type : excType .String (),
221+ Value : shortTypename ,
222+ }
223+
224+ // Refer to the exception payload in the Message field.
225+ //
226+ // We only add a numeric counter for every exception *after* the
227+ // first one. This is because the 1st exception payload is
228+ // special, it is used as report title for Sentry and we don't
229+ // want to pollute that title with a counter.
230+ if len (exceptions ) == 0 {
231+ longMsgBuf .WriteString (" (top exception)" )
232+ } else {
233+ counterStr := fmt .Sprintf ("(%d)" , extraNum )
234+ extraNum ++
235+ exc .Type = counterStr + " " + exc .Type
236+ fmt .Fprintf (& longMsgBuf , " %s" , counterStr )
237+ }
104238
105- exceptions = append (exceptions , st )
239+ exceptions = append (exceptions , exc )
106240 } else {
107- // No: are there details? If so, print them.
108- // Note: we only print the details if no stack trace
109- // was found that that level. This is because
110- // stack trace annotations also produce the stack
111- // trace as safe detail string.
241+ // No stack trace.
242+ // Are there safe details? If so, print them.
243+ //
244+ // Note: we only print the details if no stack trace was found
245+ // at that level. This is because stack trace annotations also
246+ // produce the stack trace as safe detail string.
112247 genExtra = len (details [i ].SafeDetails ) > 1
113248 if len (details [i ].SafeDetails ) > 0 {
114249 d := details [i ].SafeDetails [0 ]
@@ -121,9 +256,9 @@ func BuildSentryReport(err error) (event *sentry.Event, extraDetails map[string]
121256 if d != "" {
122257 longMsgBuf .WriteString (": " )
123258 longMsgBuf .WriteString (d )
124- if firstError == nil {
259+ if firstDetailLine == "" {
125260 // Keep the string for later.
126- firstError = & d
261+ firstDetailLine = d
127262 }
128263 }
129264 }
@@ -142,33 +277,72 @@ func BuildSentryReport(err error) (event *sentry.Event, extraDetails map[string]
142277 }
143278 }
144279
145- // Determine a head message for the report.
146- headMsg := "<unknown error>"
147- if firstError != nil {
148- headMsg = * firstError
149- }
150- // Prepend the "main" source line information if available/found.
151- if file , line , fn , ok := withstack .GetOneLineSource (err ); ok {
152- headMsg = fmt .Sprintf ("%s:%d: %s: %s" , file , line , fn , headMsg )
280+ if extraNum > 1 {
281+ // Make the message part more informational.
282+ longMsgBuf .WriteString ("\n (check the extra data payloads)" )
153283 }
154284
285+ // Produce the full error type description.
155286 extras ["error types" ] = typesBuf .String ()
156287
157- // Make the message part more informational.
158- longMsgBuf .WriteString ("\n (check the extra data payloads)" )
159- extras ["long message" ] = longMsgBuf .String ()
288+ // Sentry is mightily annoying.
289+ reverseExceptionOrder (exceptions )
160290
291+ // Start assembling the event.
161292 event = sentry .NewEvent ()
162- event .Message = headMsg
163-
164- module := domains .GetDomain (err )
165- for _ , exception := range exceptions {
166- event .Exception = append (event .Exception ,
167- sentry.Exception {
168- Type : "<reported error>" ,
169- Module : string (module ),
170- Stacktrace : exception ,
171- })
293+ event .Message = longMsgBuf .String ()
294+ event .Exception = exceptions
295+
296+ // If there is no exception payload, synthetize one.
297+ if len (event .Exception ) == 0 {
298+ // We know we don't have a stack trace to extract line/function
299+ // info from (if we had, we'd have an Exception payload at that
300+ // point). Instead, we make a best effort using bits and pieces
301+ // assembled so far.
302+ event .Exception = append (event .Exception , sentry.Exception {
303+ Module : module ,
304+ Type : leafErrorType ,
305+ Value : firstDetailLine ,
306+ })
307+ } else {
308+ // We have at least one exception payload already. In that case,
309+ // decorate the first exception with the first detail line if
310+ // there is one. This enhances the title of the Sentry report.
311+ //
312+ // This goes from:
313+ // <file> (func)
314+ // <type>
315+ //
316+ // to:
317+ // <file> (func)
318+ // wrapped <leaftype>[: <detail>]
319+ // via <type>
320+ // if wrapped; or if leaf:
321+ // <file> (func)
322+ // <leaftype>[: <detail>]
323+
324+ var newValueBuf strings.Builder
325+ // Note that "first exception" is the last item in the slice,
326+ // because... Sentry is annoying.
327+ firstExc := & event .Exception [len (event .Exception )- 1 ]
328+ // Add the leaf error type if different from the type at this
329+ // level (this is going to be the common case, unless using
330+ // pkg/errors.WithStack).
331+ wrapped := false
332+ if firstExc .Value == leafErrorType {
333+ newValueBuf .WriteString (firstExc .Value )
334+ } else {
335+ newValueBuf .WriteString (leafErrorType )
336+ wrapped = true
337+ }
338+ // Add the detail info line, if any.
339+ if firstDetailLine != "" {
340+ fmt .Fprintf (& newValueBuf , ": %s" , firstDetailLine )
341+ }
342+ if wrapped {
343+ fmt .Fprintf (& newValueBuf , "\n via %s" , firstExc .Value )
344+ }
345+ firstExc .Value = newValueBuf .String ()
172346 }
173347
174348 return event , extras
@@ -206,10 +380,16 @@ func ReportError(err error) (eventID string) {
206380 return
207381}
208382
209- func simpleErrType (tn string ) string {
383+ func lastPathComponent (tn string ) string {
210384 // Strip the path prefix.
211385 if i := strings .LastIndexByte (tn , '/' ); i >= 0 {
212386 tn = tn [i + 1 :]
213387 }
214388 return tn
215389}
390+
391+ func reverseExceptionOrder (ex []sentry.Exception ) {
392+ for i := 0 ; i < len (ex )/ 2 ; i ++ {
393+ ex [i ], ex [len (ex )- i - 1 ] = ex [len (ex )- i - 1 ], ex [i ]
394+ }
395+ }
0 commit comments