Skip to content

chore: add unknown usage event type error #19436

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 36 additions & 13 deletions coderd/usage/usagetypes/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ package usagetypes
import (
"bytes"
"encoding/json"
"fmt"
"strings"

"golang.org/x/xerrors"
Expand All @@ -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"
)
Expand All @@ -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)}
}
}

Expand Down
27 changes: 17 additions & 10 deletions coderd/usage/usagetypes/events_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})
}

Expand All @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions enterprise/coderd/usage/publisher.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"golang.org/x/xerrors"

"cdr.dev/slog"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbtime"
Expand Down Expand Up @@ -396,6 +397,7 @@ func (p *tallymanPublisher) sendPublishRequest(ctx context.Context, deploymentID
if err != nil {
return usagetypes.TallymanV1IngestResponse{}, err
}
r.Header.Set("User-Agent", "coderd/"+buildinfo.Version())
r.Header.Set(usagetypes.TallymanCoderLicenseKeyHeader, licenseJwt)
r.Header.Set(usagetypes.TallymanCoderDeploymentIDHeader, deploymentID.String())

Expand Down
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy