Skip to content

Commit 489491a

Browse files
authored
Merge pull request #34 from knz/20200522-error-report
2 parents 9833fdf + d6e8831 commit 489491a

File tree

2 files changed

+254
-78
lines changed

2 files changed

+254
-78
lines changed

report/report.go

Lines changed: 245 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
//
3196
func 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, "\nvia %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

Comments
 (0)