chore: add unknown usage event type error (#19436)

- Adds `usagetypes.UnknownEventTypeError` type, which is returned by
`ParseEventWithType`
- Changes `ParseEvent` to not be a generic function since it doesn't
really need it
- Adds `User-Agent` to tallyman requests
This commit is contained in:
Dean Sheather
2025-08-22 16:32:35 +10:00
committed by GitHub
parent b90bc7c398
commit 82f2e15974
3 changed files with 55 additions and 23 deletions
+36 -13
View File
@@ -13,6 +13,7 @@ package usagetypes
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"golang.org/x/xerrors"
@@ -22,6 +23,10 @@ import (
// type `usage_event_type`.
type UsageEventType string
// All event types.
//
// When adding a new event type, ensure you add it to the Valid method and the
// ParseEventWithType function.
const (
UsageEventTypeDCManagedAgentsV1 UsageEventType = "dc_managed_agents_v1"
)
@@ -43,38 +48,56 @@ func (e UsageEventType) IsHeartbeat() bool {
return e.Valid() && strings.HasPrefix(string(e), "hb_")
}
// ParseEvent parses the raw event data into the specified Go type. It fails if
// there is any unknown fields or extra data after the event. The returned event
// is validated.
func ParseEvent[T Event](data json.RawMessage) (T, error) {
// ParseEvent parses the raw event data into the provided event. It fails if
// there is any unknown fields or extra data at the end of the JSON. The
// returned event is validated.
func ParseEvent(data json.RawMessage, out Event) error {
dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields()
var event T
err := dec.Decode(&event)
err := dec.Decode(out)
if err != nil {
return event, xerrors.Errorf("unmarshal %T event: %w", event, err)
return xerrors.Errorf("unmarshal %T event: %w", out, err)
}
if dec.More() {
return event, xerrors.Errorf("extra data after %T event", event)
return xerrors.Errorf("extra data after %T event", out)
}
err = event.Valid()
err = out.Valid()
if err != nil {
return event, xerrors.Errorf("invalid %T event: %w", event, err)
return xerrors.Errorf("invalid %T event: %w", out, err)
}
return event, nil
return nil
}
// UnknownEventTypeError is returned by ParseEventWithType when an unknown event
// type is encountered.
type UnknownEventTypeError struct {
EventType string
}
var _ error = UnknownEventTypeError{}
// Error implements error.
func (e UnknownEventTypeError) Error() string {
return fmt.Sprintf("unknown usage event type: %q", e.EventType)
}
// ParseEventWithType parses the raw event data into the specified Go type. It
// fails if there is any unknown fields or extra data after the event. The
// returned event is validated.
//
// If the event type is unknown, UnknownEventTypeError is returned.
func ParseEventWithType(eventType UsageEventType, data json.RawMessage) (Event, error) {
switch eventType {
case UsageEventTypeDCManagedAgentsV1:
return ParseEvent[DCManagedAgentsV1](data)
var event DCManagedAgentsV1
if err := ParseEvent(data, &event); err != nil {
return nil, err
}
return event, nil
default:
return nil, xerrors.Errorf("unknown event type: %s", eventType)
return nil, UnknownEventTypeError{EventType: string(eventType)}
}
}
+17 -10
View File
@@ -13,29 +13,34 @@ func TestParseEvent(t *testing.T) {
t.Run("ExtraFields", func(t *testing.T) {
t.Parallel()
_, err := usagetypes.ParseEvent[usagetypes.DCManagedAgentsV1]([]byte(`{"count": 1, "extra": "field"}`))
require.ErrorContains(t, err, "unmarshal usagetypes.DCManagedAgentsV1 event")
var event usagetypes.DCManagedAgentsV1
err := usagetypes.ParseEvent([]byte(`{"count": 1, "extra": "field"}`), &event)
require.ErrorContains(t, err, "unmarshal *usagetypes.DCManagedAgentsV1 event")
})
t.Run("ExtraData", func(t *testing.T) {
t.Parallel()
_, err := usagetypes.ParseEvent[usagetypes.DCManagedAgentsV1]([]byte(`{"count": 1}{"count": 2}`))
require.ErrorContains(t, err, "extra data after usagetypes.DCManagedAgentsV1 event")
var event usagetypes.DCManagedAgentsV1
err := usagetypes.ParseEvent([]byte(`{"count": 1}{"count": 2}`), &event)
require.ErrorContains(t, err, "extra data after *usagetypes.DCManagedAgentsV1 event")
})
t.Run("DCManagedAgentsV1", func(t *testing.T) {
t.Parallel()
event, err := usagetypes.ParseEvent[usagetypes.DCManagedAgentsV1]([]byte(`{"count": 1}`))
var event usagetypes.DCManagedAgentsV1
err := usagetypes.ParseEvent([]byte(`{"count": 1}`), &event)
require.NoError(t, err)
require.Equal(t, usagetypes.DCManagedAgentsV1{Count: 1}, event)
require.Equal(t, map[string]any{"count": uint64(1)}, event.Fields())
_, err = usagetypes.ParseEvent[usagetypes.DCManagedAgentsV1]([]byte(`{"count": "invalid"}`))
require.ErrorContains(t, err, "unmarshal usagetypes.DCManagedAgentsV1 event")
event = usagetypes.DCManagedAgentsV1{}
err = usagetypes.ParseEvent([]byte(`{"count": "invalid"}`), &event)
require.ErrorContains(t, err, "unmarshal *usagetypes.DCManagedAgentsV1 event")
_, err = usagetypes.ParseEvent[usagetypes.DCManagedAgentsV1]([]byte(`{}`))
require.ErrorContains(t, err, "invalid usagetypes.DCManagedAgentsV1 event: count must be greater than 0")
event = usagetypes.DCManagedAgentsV1{}
err = usagetypes.ParseEvent([]byte(`{}`), &event)
require.ErrorContains(t, err, "invalid *usagetypes.DCManagedAgentsV1 event: count must be greater than 0")
})
}
@@ -45,7 +50,9 @@ func TestParseEventWithType(t *testing.T) {
t.Run("UnknownEvent", func(t *testing.T) {
t.Parallel()
_, err := usagetypes.ParseEventWithType(usagetypes.UsageEventType("fake"), []byte(`{}`))
require.ErrorContains(t, err, "unknown event type: fake")
var unknownEventTypeError usagetypes.UnknownEventTypeError
require.ErrorAs(t, err, &unknownEventTypeError)
require.Equal(t, "fake", unknownEventTypeError.EventType)
})
t.Run("DCManagedAgentsV1", func(t *testing.T) {