Skip to content

Commit 5c97548

Browse files
committed
fix: allow string and int types for backtrace number field
1 parent 929888e commit 5c97548

File tree

2 files changed

+173
-2
lines changed

2 files changed

+173
-2
lines changed

internal/hbapi/types.go

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,32 @@
11
package hbapi
22

3-
import "time"
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"time"
7+
)
8+
9+
// StringOrInt is a custom type that can unmarshal from either a string or integer JSON value
10+
type StringOrInt string
11+
12+
// UnmarshalJSON implements json.Unmarshaler interface to handle both string and integer values
13+
func (s *StringOrInt) UnmarshalJSON(data []byte) error {
14+
// Try to unmarshal as string first
15+
var str string
16+
if err := json.Unmarshal(data, &str); err == nil {
17+
*s = StringOrInt(str)
18+
return nil
19+
}
20+
21+
// If that fails, try as integer
22+
var num int
23+
if err := json.Unmarshal(data, &num); err == nil {
24+
*s = StringOrInt(fmt.Sprintf("%d", num))
25+
return nil
26+
}
27+
28+
return fmt.Errorf("StringOrInt: cannot unmarshal %s into string or integer", data)
29+
}
430

531
// User represents a Honeybadger user
632
type User struct {
@@ -95,7 +121,7 @@ type NoticeRequest struct {
95121

96122
// BacktraceEntry represents a single entry in the error backtrace
97123
type BacktraceEntry struct {
98-
Number string `json:"number"`
124+
Number StringOrInt `json:"number"`
99125
File string `json:"file"`
100126
Method string `json:"method"`
101127
Source map[string]interface{} `json:"source,omitempty"`

internal/hbapi/types_test.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package hbapi
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
)
7+
8+
func TestStringOrInt_UnmarshalJSON(t *testing.T) {
9+
tests := []struct {
10+
name string
11+
input string
12+
want string
13+
wantErr bool
14+
}{
15+
{
16+
name: "string value",
17+
input: `"123"`,
18+
want: "123",
19+
wantErr: false,
20+
},
21+
{
22+
name: "integer value",
23+
input: `123`,
24+
want: "123",
25+
wantErr: false,
26+
},
27+
{
28+
name: "zero integer",
29+
input: `0`,
30+
want: "0",
31+
wantErr: false,
32+
},
33+
{
34+
name: "negative integer",
35+
input: `-1`,
36+
want: "-1",
37+
wantErr: false,
38+
},
39+
{
40+
name: "boolean value should fail",
41+
input: `true`,
42+
wantErr: true,
43+
},
44+
{
45+
name: "object value should fail",
46+
input: `{}`,
47+
wantErr: true,
48+
},
49+
}
50+
51+
for _, tt := range tests {
52+
t.Run(tt.name, func(t *testing.T) {
53+
var s StringOrInt
54+
err := json.Unmarshal([]byte(tt.input), &s)
55+
if (err != nil) != tt.wantErr {
56+
t.Errorf("StringOrInt.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
57+
return
58+
}
59+
if !tt.wantErr && string(s) != tt.want {
60+
t.Errorf("StringOrInt.UnmarshalJSON() = %v, want %v", s, tt.want)
61+
}
62+
})
63+
}
64+
}
65+
66+
func TestBacktraceEntry_UnmarshalJSON(t *testing.T) {
67+
tests := []struct {
68+
name string
69+
input string
70+
want BacktraceEntry
71+
wantErr bool
72+
}{
73+
{
74+
name: "number as string",
75+
input: `{
76+
"number": "42",
77+
"file": "/path/to/file.js",
78+
"method": "someMethod"
79+
}`,
80+
want: BacktraceEntry{
81+
Number: "42",
82+
File: "/path/to/file.js",
83+
Method: "someMethod",
84+
},
85+
wantErr: false,
86+
},
87+
{
88+
name: "number as integer",
89+
input: `{
90+
"number": 42,
91+
"file": "/path/to/file.js",
92+
"method": "someMethod"
93+
}`,
94+
want: BacktraceEntry{
95+
Number: "42",
96+
File: "/path/to/file.js",
97+
Method: "someMethod",
98+
},
99+
wantErr: false,
100+
},
101+
{
102+
name: "complete backtrace entry with source",
103+
input: `{
104+
"number": 10,
105+
"file": "/app/index.js",
106+
"method": "handleError",
107+
"source": {"10": "throw new Error();"},
108+
"context": "app"
109+
}`,
110+
want: BacktraceEntry{
111+
Number: "10",
112+
File: "/app/index.js",
113+
Method: "handleError",
114+
Source: map[string]interface{}{"10": "throw new Error();"},
115+
Context: "app",
116+
},
117+
wantErr: false,
118+
},
119+
}
120+
121+
for _, tt := range tests {
122+
t.Run(tt.name, func(t *testing.T) {
123+
var entry BacktraceEntry
124+
err := json.Unmarshal([]byte(tt.input), &entry)
125+
if (err != nil) != tt.wantErr {
126+
t.Errorf("BacktraceEntry.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
127+
return
128+
}
129+
if !tt.wantErr {
130+
if string(entry.Number) != string(tt.want.Number) {
131+
t.Errorf("BacktraceEntry.Number = %v, want %v", entry.Number, tt.want.Number)
132+
}
133+
if entry.File != tt.want.File {
134+
t.Errorf("BacktraceEntry.File = %v, want %v", entry.File, tt.want.File)
135+
}
136+
if entry.Method != tt.want.Method {
137+
t.Errorf("BacktraceEntry.Method = %v, want %v", entry.Method, tt.want.Method)
138+
}
139+
if entry.Context != tt.want.Context {
140+
t.Errorf("BacktraceEntry.Context = %v, want %v", entry.Context, tt.want.Context)
141+
}
142+
}
143+
})
144+
}
145+
}

0 commit comments

Comments
 (0)