-
Notifications
You must be signed in to change notification settings - Fork 372
feat(analytics): support explicit count values in Count method #3619
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -147,11 +147,27 @@ func (p *PosthogAnalytics) Count(ctx context.Context, resource analytics.Resourc | |
| merged[k] = v | ||
| } | ||
| } | ||
|
|
||
| // Extract count from last properties arg if multiple are provided. | ||
| // Allows passing aggregated counts instead of always using 1. | ||
| var n int64 | ||
| if len(props) > 1 && props[len(props)-1] != nil { | ||
| for _, v := range props[len(props)-1] { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unsure this is desirable due to the typing of Like, a.Count(ctx, analytics.Log, analytics.Get, analytics.Properties{}, analytics.Properties{"foo": 5}))Also, if we pass in two (or more) properties, the caller has no way of knowing the final arg will have all of its values used for incrementing. This ambigious behaviour could lead to unintentionally passing props in that we expect to be included as tags/labels but get used to increment metrics. |
||
| if value, ok := v.(int64); ok { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This conversion means we're only ever supporting an i.e a.Count(ctx, analytics.Log, analytics.Get, analytics.Properties{}, analytics.Properties{"foo": uint64(5)}))Would see |
||
| n += value | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if n == 0 { | ||
| n = 1 | ||
| } | ||
|
|
||
| if source := analytics.SourceFromContext(ctx); source != "" { | ||
| merged["source"] = string(source) | ||
| } | ||
|
|
||
| p.aggregator.Count(resource, action, tid, tokenID, 1, merged) | ||
| p.aggregator.Count(resource, action, tid, tokenID, n, merged) | ||
| } | ||
|
|
||
| func (p *PosthogAnalytics) flushCount(resource analytics.Resource, action analytics.Action, tenantID uuid.UUID, tokenID *uuid.UUID, count int64, properties analytics.Properties) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,223 @@ | ||
| package posthog | ||
|
|
||
| import ( | ||
| "context" | ||
| "sync" | ||
| "testing" | ||
|
|
||
| "github.com/google/uuid" | ||
| "github.com/rs/zerolog" | ||
|
|
||
| "github.com/hatchet-dev/hatchet/pkg/analytics" | ||
| ) | ||
|
|
||
| var testLogger = zerolog.Nop() | ||
|
|
||
| type countCall struct { | ||
| Resource analytics.Resource | ||
| Action analytics.Action | ||
| TenantID uuid.UUID | ||
| TokenID *uuid.UUID | ||
| Count int64 | ||
| Properties analytics.Properties | ||
| } | ||
|
|
||
| type countRecorder struct { | ||
| mu sync.Mutex | ||
| calls []countCall | ||
| } | ||
|
|
||
| func (r *countRecorder) record(resource analytics.Resource, action analytics.Action, tenantID uuid.UUID, tokenID *uuid.UUID, count int64, properties analytics.Properties) { | ||
| r.mu.Lock() | ||
| defer r.mu.Unlock() | ||
| r.calls = append(r.calls, countCall{ | ||
| Resource: resource, | ||
| Action: action, | ||
| TenantID: tenantID, | ||
| TokenID: tokenID, | ||
| Count: count, | ||
| Properties: properties, | ||
| }) | ||
| } | ||
|
|
||
| func (r *countRecorder) getCalls() []countCall { | ||
| r.mu.Lock() | ||
| defer r.mu.Unlock() | ||
| cp := make([]countCall, len(r.calls)) | ||
| copy(cp, r.calls) | ||
| return cp | ||
| } | ||
|
|
||
| func TestCount_DefaultToOne(t *testing.T) { | ||
| rec := &countRecorder{} | ||
| agg := analytics.NewAggregator(&testLogger, true, 50, 0, rec.record) | ||
| agg.Start() | ||
| p := &PosthogAnalytics{ | ||
| aggregator: agg, | ||
| } | ||
|
|
||
| ctx := context.Background() | ||
| p.Count(ctx, analytics.Event, analytics.Create) | ||
|
|
||
| agg.Shutdown() | ||
|
|
||
| calls := rec.getCalls() | ||
| if len(calls) != 1 { | ||
| t.Fatalf("expected 1 call, got %d", len(calls)) | ||
| } | ||
| if calls[0].Count != 1 { | ||
| t.Errorf("expected count 1, got %d", calls[0].Count) | ||
| } | ||
| } | ||
|
|
||
| func TestCount_SinglePropertyFallbackToOne(t *testing.T) { | ||
| rec := &countRecorder{} | ||
| agg := analytics.NewAggregator(&testLogger, true, 50, 0, rec.record) | ||
| agg.Start() | ||
| p := &PosthogAnalytics{ | ||
| aggregator: agg, | ||
| } | ||
|
|
||
| ctx := context.Background() | ||
| p.Count(ctx, analytics.Event, analytics.Create, analytics.Props( | ||
| "has_priority", true, | ||
| )) | ||
|
|
||
| agg.Shutdown() | ||
|
|
||
| calls := rec.getCalls() | ||
| if len(calls) != 1 { | ||
| t.Fatalf("expected 1 call, got %d", len(calls)) | ||
| } | ||
| if calls[0].Count != 1 { | ||
| t.Errorf("expected count 1, got %d", calls[0].Count) | ||
| } | ||
| } | ||
|
|
||
| func TestCount_ExplicitCountFromLastProperties(t *testing.T) { | ||
| rec := &countRecorder{} | ||
| agg := analytics.NewAggregator(&testLogger, true, 50, 0, rec.record) | ||
| agg.Start() | ||
| p := &PosthogAnalytics{ | ||
| aggregator: agg, | ||
| } | ||
|
|
||
| ctx := context.Background() | ||
| p.Count(ctx, analytics.Event, analytics.Create, | ||
| analytics.Props("has_priority", true), | ||
| analytics.Props("count", int64(5)), | ||
| ) | ||
|
|
||
| agg.Shutdown() | ||
|
|
||
| calls := rec.getCalls() | ||
| if len(calls) != 1 { | ||
| t.Fatalf("expected 1 call, got %d", len(calls)) | ||
| } | ||
| if calls[0].Count != 5 { | ||
| t.Errorf("expected count 5, got %d", calls[0].Count) | ||
| } | ||
| } | ||
|
|
||
| func TestCount_SumMultipleInt64Values(t *testing.T) { | ||
| rec := &countRecorder{} | ||
| agg := analytics.NewAggregator(&testLogger, true, 50, 0, rec.record) | ||
| agg.Start() | ||
| p := &PosthogAnalytics{ | ||
| aggregator: agg, | ||
| } | ||
|
|
||
| ctx := context.Background() | ||
| p.Count(ctx, analytics.Event, analytics.Create, | ||
| analytics.Props("has_priority", true), | ||
| analytics.Props("count", int64(2), "other_count", int64(3)), | ||
| ) | ||
|
|
||
| agg.Shutdown() | ||
|
|
||
| calls := rec.getCalls() | ||
| if len(calls) != 1 { | ||
| t.Fatalf("expected 1 call, got %d", len(calls)) | ||
| } | ||
| if calls[0].Count != 5 { | ||
| t.Errorf("expected count 5 (sum of 2+3), got %d", calls[0].Count) | ||
| } | ||
| } | ||
|
|
||
| func TestCount_IgnoresNonInt64Values(t *testing.T) { | ||
| rec := &countRecorder{} | ||
| agg := analytics.NewAggregator(&testLogger, true, 50, 0, rec.record) | ||
| agg.Start() | ||
| p := &PosthogAnalytics{ | ||
| aggregator: agg, | ||
| } | ||
|
|
||
| ctx := context.Background() | ||
| p.Count(ctx, analytics.Event, analytics.Create, | ||
| analytics.Props("has_priority", true), | ||
| analytics.Props("count", int64(2), "name", "test", "active", true), | ||
| ) | ||
|
|
||
| agg.Shutdown() | ||
|
|
||
| calls := rec.getCalls() | ||
| if len(calls) != 1 { | ||
| t.Fatalf("expected 1 call, got %d", len(calls)) | ||
| } | ||
| if calls[0].Count != 2 { | ||
| t.Errorf("expected count 2, got %d", calls[0].Count) | ||
| } | ||
| } | ||
|
|
||
| func TestCount_NilLastPropertiesFallbackToOne(t *testing.T) { | ||
| rec := &countRecorder{} | ||
| agg := analytics.NewAggregator(&testLogger, true, 50, 0, rec.record) | ||
| agg.Start() | ||
| p := &PosthogAnalytics{ | ||
| aggregator: agg, | ||
| } | ||
|
|
||
| ctx := context.Background() | ||
| p.Count(ctx, analytics.Event, analytics.Create, | ||
| analytics.Props("has_priority", true), | ||
| nil, | ||
| ) | ||
|
|
||
| agg.Shutdown() | ||
|
|
||
| calls := rec.getCalls() | ||
| if len(calls) != 1 { | ||
| t.Fatalf("expected 1 call, got %d", len(calls)) | ||
| } | ||
| if calls[0].Count != 1 { | ||
| t.Errorf("expected count 1, got %d", calls[0].Count) | ||
| } | ||
| } | ||
|
|
||
| func TestCount_FirstPropertyPassedToAggregator(t *testing.T) { | ||
| rec := &countRecorder{} | ||
| agg := analytics.NewAggregator(&testLogger, true, 50, 0, rec.record) | ||
| agg.Start() | ||
| p := &PosthogAnalytics{ | ||
| aggregator: agg, | ||
| } | ||
|
|
||
| ctx := context.Background() | ||
| p.Count(ctx, analytics.Event, analytics.Create, | ||
| analytics.Props("has_priority", true, "source", "api"), | ||
| analytics.Props("count", int64(10)), | ||
| ) | ||
|
|
||
| agg.Shutdown() | ||
|
|
||
| calls := rec.getCalls() | ||
| if len(calls) != 1 { | ||
| t.Fatalf("expected 1 call, got %d", len(calls)) | ||
| } | ||
| if calls[0].Properties["has_priority"] != true { | ||
| t.Error("expected has_priority=true in properties") | ||
| } | ||
| if calls[0].Properties["source"] != "api" { | ||
| t.Error("expected source=api in properties") | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tbh I still think we should be passing in a structured type to determine/override this value, where necessary instead of relying on a positional arg in the varadic params.
Like, it's more explicit that way i.e
versus:
where the latter could be error prone as well since it means we'd need to document this positional behaviour.
This typed-approach also means we can extend supported operations i.e
Then if we have other metric types (i.e a gauge or whatever) we can extend this approach to suit.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough,
will move with original approach of using structured property, which ig will also solve to other concerns automatically.