carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package apperr
2
3import (
4 "crypto/sha256"
5 "encoding/hex"
6 "regexp"
7 "strconv"
8 "strings"
9)
10
11var (
12 reQuoted = regexp.MustCompile(`"[^"]*"|'[^']*'`)
13 reUUIDAny = regexp.MustCompile(`\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b`)
14 reHexRun = regexp.MustCompile(`\b[0-9a-fA-F]{8,}\b`)
15 reDigits4 = regexp.MustCompile(`\b\d{4,}\b`)
16 reLineCol = regexp.MustCompile(`:\d+(:\d+)?`)
17 reBundleJS = regexp.MustCompile(`\.[0-9a-fA-F]{8,}\.js`)
18)
19
20// Normalize folds the parts of a message that vary per occurrence into
21// placeholders on token boundaries (spec §3.2): quoted strings, UUIDs, hex
22// runs of 8+, digit runs of 4+. Tokens of 1–3 digits and mixed tokens
23// (v2, E404) survive, so "HTTP 503" and "retry 3" still separate groups
24// while "user 4123" and "user 9911" do not. Small ids under 1,000 will
25// split groups; that is the stated trade-off, and a caller-supplied FP
26// overrides it.
27func Normalize(s string) string {
28 s = reQuoted.ReplaceAllString(s, `"?"`)
29 s = reUUIDAny.ReplaceAllString(s, "<uuid>")
30 s = reHexRun.ReplaceAllStringFunc(s, func(m string) string {
31 if _, err := strconv.ParseUint(m, 10, 64); err == nil {
32 return "<n>" // all digits: a digit run, not a hex run
33 }
34 return "<hex>"
35 })
36 s = reDigits4.ReplaceAllString(s, "<n>")
37 return s
38}
39
40// TopFrame is the first non-empty line of a stack with its line, column
41// and bundle hash removed, so a redeploy does not split every group.
42func TopFrame(stack string) string {
43 for _, line := range strings.Split(stack, "\n") {
44 line = strings.TrimSpace(line)
45 if line == "" {
46 continue
47 }
48 line = reBundleJS.ReplaceAllString(line, ".js")
49 line = reLineCol.ReplaceAllString(line, "")
50 return line
51 }
52 return ""
53}
54
55// Fingerprint groups occurrences: sha256 over length-delimited (source,
56// kind, Normalize(name), Normalize(TopFrame(stack)))[:16] as hex. For a
57// stack with a message line first (JS "TypeError: x\n at …"), the top
58// frame is the second line; callers pass the message as name and the
59// whole stack here.
60func Fingerprint(source, kind, name, stack string) string {
61 h := sha256.New()
62 frame := TopFrame(stack)
63 if strings.HasPrefix(frame, name) || strings.Contains(frame, ": "+name) {
64 // The stack's first line restates the message; take the next.
65 rest := strings.TrimSpace(stack)
66 if i := strings.IndexByte(rest, '\n'); i >= 0 {
67 frame = TopFrame(rest[i+1:])
68 }
69 }
70 for _, part := range []string{source, kind, Normalize(name), Normalize(frame)} {
71 h.Write([]byte(strconv.Itoa(len(part))))
72 h.Write([]byte{':'})
73 h.Write([]byte(part))
74 }
75 return hex.EncodeToString(h.Sum(nil))[:16]
76}
77