Skip to content

Commit a0a1027

Browse files
committed
fix: make User and Tenant support parameters of type string key/value pairs, map[string]any and slog
1 parent 1a99714 commit a0a1027

4 files changed

Lines changed: 126 additions & 28 deletions

File tree

README.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -201,9 +201,9 @@ The `oops.OopsError` builder must finish with either `.Errorf(...)`, `.Wrap(...)
201201
| `.Trace(string)` | `err.Trace() string` | Add a transaction id, trace id, correlation id... (default: ULID) |
202202
| `.Span(string)` | `err.Span() string` | Add a span representing a unit of work or operation... (default: ULID) |
203203
| `.Hint(string)` | `err.Hint() string` | Set a hint for faster debugging |
204-
| `.Owner(string)` | `err.Owner() (string)` | Set the name/email of the colleague/team responsible for handling this error. Useful for alerting purpose |
205-
| `.User(string, any...)` | `err.User() (string, map[string]any)` | Supply user id and a chain of key/value |
206-
| `.Tenant(string, any...)` | `err.Tenant() (string, map[string]any)` | Supply tenant id and a chain of key/value |
204+
| `.Owner(string)` | `err.Owner() (string)` | Set the name/email of the colleague/team responsible for handling this error. Useful for alerting purpose |
205+
| `.User(string, any...)` | `err.User() (string, any...)` | Supply user id with optional attributes (string key/value pairs, `map[string]any`, and `slog.Attr`) |
206+
| `.Tenant(string, any...)` | `err.Tenant() (string, any...)` | Supply tenant id with optional attributes (string key/value pairs, `map[string]any`, and `slog.Attr`) |
207207
| `.Request(*http.Request, bool)` | `err.Request() *http.Request` | Supply http request |
208208
| `.Response(*http.Response, bool)` | `err.Response() *http.Response` | Supply http response |
209209
| `.FromContext(context.Context)` | | Reuse an existing OopsErrorBuilder transported in a Go context |
@@ -257,10 +257,19 @@ err7 := oops.
257257
User(userID, "firstname", "Samuel").
258258
Errorf("could not fetch user")
259259

260+
// with map and slog.Attr user data
261+
err7b := oops.
262+
User(userID,
263+
map[string]any{"plan": "pro"},
264+
slog.String("email", "samuel@example.com")).
265+
Errorf("could not fetch user")
266+
260267
// with optional user and tenant
261268
err8 := oops.
262269
User(userID, "firstname", "Samuel").
263-
Tenant(workspaceID, "name", "my little project").
270+
Tenant(workspaceID,
271+
map[string]any{"name": "my little project"},
272+
slog.String("country", "fr")).
264273
Errorf("could not fetch user")
265274

266275
// with optional http request and response

builder.go

Lines changed: 75 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"log/slog"
78
"net/http"
89
"time"
910

@@ -514,15 +515,8 @@ func (o OopsErrorBuilder) Owner(owner string) OopsErrorBuilder {
514515
func (o OopsErrorBuilder) User(userID string, userData ...any) OopsErrorBuilder {
515516
o2 := o.copy()
516517
o2.userID = userID
517-
518-
// Process user data key-value pairs
519-
for i := 0; i < len(userData); i += 2 {
520-
if i+1 < len(userData) {
521-
key, ok := userData[i].(string)
522-
if ok {
523-
o2.userData[key] = userData[i+1]
524-
}
525-
}
518+
for key, value := range userArgsToMap(userData) {
519+
o2.userData[key] = value
526520
}
527521

528522
return o2
@@ -537,18 +531,83 @@ func (o OopsErrorBuilder) User(userID string, userData ...any) OopsErrorBuilder
537531
func (o OopsErrorBuilder) Tenant(tenantID string, tenantData ...any) OopsErrorBuilder {
538532
o2 := o.copy()
539533
o2.tenantID = tenantID
534+
for key, value := range userArgsToMap(tenantData) {
535+
o2.tenantData[key] = value
536+
}
540537

541-
// Process tenant data key-value pairs
542-
for i := 0; i < len(tenantData); i += 2 {
543-
if i+1 < len(tenantData) {
544-
key, ok := tenantData[i].(string)
545-
if ok {
546-
o2.tenantData[key] = tenantData[i+1]
538+
return o2
539+
}
540+
541+
// userArgsToMap converts variadic user/tenant arguments into a flat map.
542+
//
543+
// Supported inputs:
544+
// - slog.Attr values
545+
// - map[string]any values
546+
// - alternating string key/value pairs
547+
//
548+
// Unsupported values are ignored. If a trailing string key has no value,
549+
// it is ignored.
550+
func userArgsToMap(args []any) map[string]any {
551+
payload := map[string]any{}
552+
553+
for len(args) > 0 {
554+
switch value := args[0].(type) {
555+
case slog.Attr:
556+
payload[value.Key] = slogValueToAny(value.Value)
557+
args = args[1:]
558+
case map[string]any:
559+
for key, mapValue := range value {
560+
payload[key] = mapValue
547561
}
562+
args = args[1:]
563+
case string:
564+
if len(args) == 1 {
565+
return payload
566+
}
567+
568+
payload[value] = args[1]
569+
args = args[2:]
570+
default:
571+
args = args[1:]
548572
}
549573
}
550574

551-
return o2
575+
return payload
576+
}
577+
578+
// slogValueToAny converts a slog.Value into its resolved Go representation.
579+
//
580+
// Group values are converted recursively into map[string]any so they can be
581+
// stored in user/tenant payload maps.
582+
func slogValueToAny(value slog.Value) any {
583+
value = value.Resolve()
584+
585+
switch value.Kind() {
586+
case slog.KindString:
587+
return value.String()
588+
case slog.KindInt64:
589+
return value.Int64()
590+
case slog.KindUint64:
591+
return value.Uint64()
592+
case slog.KindFloat64:
593+
return value.Float64()
594+
case slog.KindBool:
595+
return value.Bool()
596+
case slog.KindDuration:
597+
return value.Duration()
598+
case slog.KindTime:
599+
return value.Time()
600+
case slog.KindGroup:
601+
group := map[string]any{}
602+
for _, attr := range value.Group() {
603+
group[attr.Key] = slogValueToAny(attr.Value)
604+
}
605+
return group
606+
case slog.KindLogValuer:
607+
return slogValueToAny(value.LogValuer().LogValue())
608+
default:
609+
return value.Any()
610+
}
552611
}
553612

554613
// Request adds HTTP request information to the error context.

oops.go

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -139,14 +139,18 @@ func Owner(owner string) OopsErrorBuilder {
139139
return newBuilder().Owner(owner)
140140
}
141141

142-
// User supplies user id and a chain of key/value.
143-
func User(userID string, data map[string]any) OopsErrorBuilder {
144-
return newBuilder().User(userID, data)
145-
}
146-
147-
// Tenant supplies tenant id and a chain of key/value.
148-
func Tenant(tenantID string, data map[string]any) OopsErrorBuilder {
149-
return newBuilder().Tenant(tenantID, data)
142+
// User supplies a user id with optional attributes.
143+
// Attributes can be provided as alternating string key/value pairs,
144+
// map[string]any values, and slog.Attr values.
145+
func User(userID string, data ...any) OopsErrorBuilder {
146+
return newBuilder().User(userID, data...)
147+
}
148+
149+
// Tenant supplies a tenant id with optional attributes.
150+
// Attributes can be provided as alternating string key/value pairs,
151+
// map[string]any values, and slog.Attr values.
152+
func Tenant(tenantID string, data ...any) OopsErrorBuilder {
153+
return newBuilder().Tenant(tenantID, data...)
150154
}
151155

152156
// Request supplies a http.Request.

oops_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,32 @@ func TestOopsUser(t *testing.T) {
340340
is.Equal(assert.AnError, err.(OopsError).err)
341341
is.Equal("user-123", err.(OopsError).userID)
342342
is.Equal(map[string]any{"firstname": "john", "lastname": "doe"}, err.(OopsError).userData)
343+
err = newBuilder().User(
344+
"user-123",
345+
slog.String("firstname", "john"),
346+
slog.Group("profile", "lastname", "doe", "age", 42),
347+
).Wrap(assert.AnError)
348+
is.Error(err)
349+
is.Equal(assert.AnError, err.(OopsError).err)
350+
is.Equal("user-123", err.(OopsError).userID)
351+
is.Equal(
352+
map[string]any{
353+
"firstname": "john",
354+
"profile": map[string]any{"lastname": "doe", "age": int64(42)},
355+
},
356+
err.(OopsError).userData,
357+
)
358+
err = newBuilder().User(
359+
"user-123",
360+
map[string]any{"firstname": "john"},
361+
"lastname",
362+
"doe",
363+
map[string]any{"country": "fr"},
364+
).Wrap(assert.AnError)
365+
is.Error(err)
366+
is.Equal(assert.AnError, err.(OopsError).err)
367+
is.Equal("user-123", err.(OopsError).userID)
368+
is.Equal(map[string]any{"firstname": "john", "lastname": "doe", "country": "fr"}, err.(OopsError).userData)
343369
}
344370

345371
func TestOopsTenant(t *testing.T) {

0 commit comments

Comments
 (0)