mirror of
https://github.com/hanzoai/insights-go.git
synced 2026-09-30 22:46:12 +00:00
The module declares go 1.27.1 and the adapter image follows. go fix applied the modernizers and left ptrString and ptrBool with no callers, so they are gone; omitzero is not applied, since it changes what the JSON says. Co-authored-by: Hanzo Dev <[email protected]>
272 lines
6.9 KiB
Go
272 lines
6.9 KiB
Go
package insights
|
|
|
|
import (
|
|
"encoding/json"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestException_Validate(t *testing.T) {
|
|
now := time.Now()
|
|
|
|
tests := map[string]struct {
|
|
msg Exception
|
|
expectedError error
|
|
}{
|
|
"valid: minimal with one valid item": {
|
|
msg: Exception{
|
|
DistinctId: "user-123",
|
|
Timestamp: now,
|
|
ExceptionList: []ExceptionItem{
|
|
{Type: "title", Value: "desc"},
|
|
},
|
|
},
|
|
expectedError: nil,
|
|
},
|
|
"error: missing DistinctId": {
|
|
msg: Exception{
|
|
Timestamp: now,
|
|
ExceptionList: []ExceptionItem{
|
|
{Type: "title", Value: "desc"},
|
|
},
|
|
},
|
|
expectedError: FieldError{
|
|
Type: "insights.Exception",
|
|
Name: "DistinctId",
|
|
Value: "",
|
|
},
|
|
},
|
|
"error: missing ExceptionList": {
|
|
msg: Exception{
|
|
DistinctId: "user-123",
|
|
Timestamp: now,
|
|
},
|
|
expectedError: FieldError{
|
|
Type: "insights.Exception",
|
|
Name: "ExceptionList",
|
|
Value: []ExceptionItem{},
|
|
},
|
|
},
|
|
"error: item missing Type": {
|
|
msg: Exception{
|
|
DistinctId: "user-123",
|
|
Timestamp: now,
|
|
ExceptionList: []ExceptionItem{
|
|
{Type: "", Value: "Bar"},
|
|
},
|
|
},
|
|
expectedError: FieldError{
|
|
Type: "insights.Exception",
|
|
Name: "Type",
|
|
Value: "",
|
|
},
|
|
},
|
|
"error: item missing Value": {
|
|
msg: Exception{
|
|
DistinctId: "user-123",
|
|
Timestamp: now,
|
|
ExceptionList: []ExceptionItem{
|
|
{Type: "Foo", Value: ""},
|
|
},
|
|
},
|
|
expectedError: FieldError{
|
|
Type: "insights.Exception",
|
|
Name: "Value",
|
|
Value: "",
|
|
},
|
|
},
|
|
"valid: full nested structure": {
|
|
msg: Exception{
|
|
DistinctId: "user-123",
|
|
Timestamp: now,
|
|
ExceptionList: []ExceptionItem{
|
|
{
|
|
Type: "MyError",
|
|
Value: "something went wrong",
|
|
Mechanism: &ExceptionMechanism{
|
|
Handled: new(true),
|
|
Synthetic: new(false),
|
|
},
|
|
Stacktrace: &ExceptionStacktrace{
|
|
Type: "raw",
|
|
Frames: []StackFrame{
|
|
{
|
|
Filename: "file.go",
|
|
LineNo: 42,
|
|
Function: "package.Sample",
|
|
InApp: true,
|
|
Synthetic: false,
|
|
Platform: "go",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedError: nil,
|
|
},
|
|
}
|
|
|
|
for name, tc := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
err := tc.msg.Validate()
|
|
if !reflect.DeepEqual(err, tc.expectedError) {
|
|
t.Errorf("expected error:\n%v \ngot:\n%v", tc.expectedError, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestException_APIfy_WithCustomProperties(t *testing.T) {
|
|
now := time.Now()
|
|
exList := []ExceptionItem{{Type: "Error", Value: "Something went wrong"}}
|
|
|
|
fingerprintStr := "custom-fingerprint"
|
|
|
|
tests := map[string]struct {
|
|
exception Exception
|
|
expectKeys map[string]any
|
|
forbiddenValues []string
|
|
}{
|
|
"basic without custom properties": {
|
|
exception: Exception{DistinctId: "user-123", Timestamp: now, ExceptionList: exList},
|
|
expectKeys: map[string]any{
|
|
"$lib": SDKName,
|
|
"$lib_version": getVersion(),
|
|
"distinct_id": "user-123",
|
|
},
|
|
},
|
|
"with custom properties and fingerprint": {
|
|
exception: Exception{
|
|
DistinctId: "user-123",
|
|
Timestamp: now,
|
|
ExceptionFingerprint: &fingerprintStr,
|
|
Properties: NewProperties().Set("environment", "production").Set("retry_count", 3),
|
|
ExceptionList: exList,
|
|
},
|
|
expectKeys: map[string]any{
|
|
"$lib": SDKName,
|
|
"$lib_version": getVersion(),
|
|
"distinct_id": "user-123",
|
|
"$exception_fingerprint": "custom-fingerprint",
|
|
"environment": "production",
|
|
"retry_count": float64(3),
|
|
},
|
|
},
|
|
"typed fields win on collision with custom": {
|
|
exception: Exception{
|
|
DistinctId: "user-123",
|
|
Timestamp: now,
|
|
Properties: NewProperties().Set("$lib", "custom-lib").Set("distinct_id", "custom-id"),
|
|
ExceptionList: exList,
|
|
},
|
|
expectKeys: map[string]any{
|
|
"$lib": SDKName,
|
|
"distinct_id": "user-123",
|
|
},
|
|
forbiddenValues: []string{"custom-lib", "custom-id"},
|
|
},
|
|
}
|
|
|
|
for name, tc := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
apiMsg, ok := tc.exception.APIfy().(ExceptionInApi)
|
|
if !ok {
|
|
t.Fatalf("expected ExceptionInApi, got %T", tc.exception.APIfy())
|
|
}
|
|
|
|
jsonBytes, err := json.Marshal(apiMsg)
|
|
if err != nil {
|
|
t.Fatalf("marshal failed: %v", err)
|
|
}
|
|
|
|
var wire map[string]any
|
|
if err := json.Unmarshal(jsonBytes, &wire); err != nil {
|
|
t.Fatalf("unmarshal failed: %v", err)
|
|
}
|
|
|
|
props, ok := wire["properties"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("properties field missing or wrong type")
|
|
}
|
|
|
|
for k, want := range tc.expectKeys {
|
|
if got := props[k]; !reflect.DeepEqual(got, want) {
|
|
t.Errorf("property %q: expected %v (%T), got %v (%T)", k, want, want, got, got)
|
|
}
|
|
}
|
|
|
|
for _, forbidden := range tc.forbiddenValues {
|
|
for k, v := range props {
|
|
if s, ok := v.(string); ok && s == forbidden {
|
|
t.Errorf("forbidden value %q leaked into property %q", forbidden, k)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestException_JSONSerialization(t *testing.T) {
|
|
fingerprint := "custom-fingerprint-123"
|
|
exception := Exception{
|
|
Type: "exception",
|
|
Uuid: "01963f1a-3c12-7b21-8a1d-3f6d1cf49b8e",
|
|
DistinctId: "user-123",
|
|
Timestamp: time.Date(2024, 11, 17, 10, 30, 0, 0, time.UTC),
|
|
Properties: NewProperties().Set("environment", "production").Set("custom_key", "custom_value"),
|
|
DisableGeoIP: true,
|
|
ExceptionFingerprint: &fingerprint,
|
|
ExceptionList: []ExceptionItem{{Type: "RuntimeError", Value: "Database connection failed"}},
|
|
}
|
|
|
|
jsonBytes, err := json.Marshal(exception.APIfy())
|
|
if err != nil {
|
|
t.Fatalf("marshal failed: %v", err)
|
|
}
|
|
|
|
var result map[string]any
|
|
if err := json.Unmarshal(jsonBytes, &result); err != nil {
|
|
t.Fatalf("unmarshal failed: %v", err)
|
|
}
|
|
|
|
props, ok := result["properties"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("properties field missing or wrong type")
|
|
}
|
|
|
|
tests := []struct {
|
|
obj map[string]any
|
|
field string
|
|
expected any
|
|
}{
|
|
{result, "type", "exception"},
|
|
{result, "uuid", "01963f1a-3c12-7b21-8a1d-3f6d1cf49b8e"},
|
|
{result, "event", "$exception"},
|
|
{result, "library", SDKName},
|
|
{result, "library_version", getVersion()},
|
|
{props, "$lib", SDKName},
|
|
{props, "$lib_version", getVersion()},
|
|
{props, "distinct_id", "user-123"},
|
|
{props, "$geoip_disable", true},
|
|
{props, "$exception_fingerprint", "custom-fingerprint-123"},
|
|
{props, "environment", "production"},
|
|
{props, "custom_key", "custom_value"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
if tc.obj[tc.field] != tc.expected {
|
|
t.Errorf("%s: expected %v, got %v", tc.field, tc.expected, tc.obj[tc.field])
|
|
}
|
|
}
|
|
|
|
exList, ok := props["$exception_list"].([]any)
|
|
if !ok {
|
|
t.Errorf("$exception_list: expected []interface{}, got %T", props["$exception_list"])
|
|
}
|
|
if len(exList) == 0 {
|
|
t.Errorf("$exception_list: expected non-empty list, got empty")
|
|
}
|
|
}
|