Feat--consistent-analytics-events#3239
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR refactors and expands server-side analytics by introducing a structured analytics API (resource/action taxonomy), adding high-throughput aggregated counting, updating the PostHog implementation, and wiring analytics emission across gRPC services and HTTP API handlers.
Changes:
- Introduces
analytics.Resource/analytics.Action, context-based API token attribution, and a newCountaggregation path. - Adds an
Aggregatorto batchCountevents and integrates it into the PostHog analytics emitter with lifecycle (Start/Close). - Instruments multiple gRPC/HTTP handlers to emit
Enqueue/Countevents and threadsAnalyticsthrough service constructors.
Reviewed changes
Copilot reviewed 33 out of 33 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/config/loader/loader.go | Initializes PostHog analytics with logger, starts it, and closes it during cleanup. |
| pkg/analytics/posthog/posthog.go | Reworks PostHog emitter to support typed events, aggregation-backed counts, logging, and Close(). |
| pkg/analytics/analytics.go | Defines Resource/Action taxonomy, context token key, helpers, and expands Analytics interface. |
| pkg/analytics/aggregating.go | Adds a batching Aggregator for high-throughput Count calls. |
| pkg/analytics/aggregating_test.go | Adds unit tests covering aggregation, eviction, shutdown flush, and concurrency. |
| internal/services/ingestor/server.go | Adds Count instrumentation for event/log/stream ingestion endpoints. |
| internal/services/ingestor/ingestor.go | Adds Analytics dependency injection to ingestor service with NoOp default. |
| internal/services/grpc/middleware/rate_limit.go | Switches rate limit token lookup to analytics.APITokenIDKey. |
| internal/services/grpc/middleware/auth.go | Stores API token UUID in context under analytics.APITokenIDKey. |
| internal/services/dispatcher/v1/server.go | Adds worker register/listen Count instrumentation. |
| internal/services/dispatcher/v1/dispatcher.go | Adds Analytics dependency injection to v1 dispatcher service with NoOp default. |
| internal/services/dispatcher/server.go | Adds worker lifecycle Count instrumentation and new props collection. |
| internal/services/dispatcher/dispatcher.go | Adds Analytics dependency injection to dispatcher implementation with NoOp default. |
| internal/services/admin/v1/server.go | Adds Count instrumentation for task/workflow operations and adds workflow feature-flag extraction. |
| internal/services/admin/v1/admin.go | Adds Analytics default (NoOp) to v1 admin service options. |
| internal/services/admin/server.go | Adds Count instrumentation for workflow/run creation and rate limit creation; adds workflow opts feature flags extraction. |
| internal/services/admin/admin.go | Adds Analytics dependency injection to admin service with NoOp default. |
| cmd/hatchet-engine/engine/run.go | Wires sc.Analytics into dispatcher/admin/ingestor constructors (v0 and v1). |
| api/v1/server/handlers/workflows/delete.go | Emits workflow delete analytics event. |
| api/v1/server/handlers/v1/webhooks/delete.go | Emits webhook delete analytics event. |
| api/v1/server/handlers/v1/webhooks/create.go | Emits webhook create analytics event. |
| api/v1/server/handlers/users/update_login.go | Emits basic login analytics event. |
| api/v1/server/handlers/users/reject_invite.go | Migrates invite reject analytics to typed Enqueue signature. |
| api/v1/server/handlers/users/google_oauth_callback.go | Emits Google OAuth login analytics event. |
| api/v1/server/handlers/users/github_oauth_callback.go | Emits GitHub OAuth login analytics event. |
| api/v1/server/handlers/users/get_current.go | Switches from event enqueue to Identify for updating user properties. |
| api/v1/server/handlers/users/create.go | Emits user create analytics event. |
| api/v1/server/handlers/users/accept_invite.go | Migrates invite accept analytics to typed Enqueue signature. |
| api/v1/server/handlers/tenants/create.go | Emits tenant create analytics event. |
| api/v1/server/handlers/tenants/create_invite.go | Migrates invite create analytics to typed Enqueue signature. |
| api/v1/server/handlers/api-tokens/revoke.go | Migrates token revoke analytics to typed Enqueue signature. |
| api/v1/server/handlers/api-tokens/create.go | Migrates token create analytics to typed Enqueue signature. |
| api/v1/server/authn/middleware.go | Captures token UUID from JWT validation and stores it for downstream use. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
…t-dev/hatchet into feat--consistent-analytics-events
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 33 out of 33 changed files in this pull request and generated 6 comments.
Comments suppressed due to low confidence (1)
internal/services/dispatcher/dispatcher.go:285
- File contains unresolved Git merge-conflict markers (e.g.
<<<<<<<,=======,>>>>>>>). This will not compile and also drops either theanalyticsassignment orstreamEventBufferTimeoutdepending on the resolution. Resolve the conflict and ensure both fields are set in the DispatcherImpl struct literal.
workflowRunBufferSize: opts.workflowRunBufferSize,
analytics: opts.analytics,
streamEventBufferTimeout: opts.streamEventBufferTimeout,
version: opts.version,
}, nil
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 35 out of 35 changed files in this pull request and generated 9 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| u.config.Analytics.Enqueue( | ||
| ctx.Request().Context(), | ||
| analytics.User, analytics.Login, | ||
| existingUser.ID.String(), | ||
| map[string]interface{}{"provider": "basic"}, | ||
| ) |
There was a problem hiding this comment.
UserUpdateLogin is an unauthenticated endpoint (no auth middleware sets analytics context keys), so ctx.Request().Context() likely lacks analytics.UserIDKey/TenantIDKey/APITokenIDKey. In that case DistinctID(...) becomes empty and the PostHog capture will fail / be dropped. Consider deriving a context with analytics.UserIDKey set to existingUser.ID (and using that for Enqueue) so login events always have a distinct id.
| u.config.Analytics.Enqueue( | ||
| ctx.Request().Context(), | ||
| analytics.User, analytics.Create, | ||
| user.ID.String(), | ||
| map[string]interface{}{"provider": "basic"}, | ||
| ) |
There was a problem hiding this comment.
UserCreate is also unauthenticated (wrapper doesn’t set Cookie/Bearer auth scopes), so ctx.Request().Context() likely doesn’t include analytics.UserIDKey. This can lead to an empty PostHog DistinctId and the signup event being dropped. Set analytics.UserIDKey in a derived context (using the newly created user.ID) before calling Enqueue.
| u.config.Analytics.Enqueue( | ||
| ctx.Request().Context(), | ||
| analytics.User, analytics.Login, | ||
| user.ID.String(), | ||
| map[string]interface{}{"provider": "google"}, | ||
| ) |
There was a problem hiding this comment.
OAuth callback routes are unauthenticated in the generated wrapper, so ctx.Request().Context() likely lacks analytics.UserIDKey when calling Enqueue. That results in an empty DistinctId and the login event being dropped. Consider creating a derived context with analytics.UserIDKey set to user.ID for the analytics call.
| u.config.Analytics.Enqueue( | ||
| ctx.Request().Context(), | ||
| analytics.User, analytics.Login, | ||
| user.ID.String(), | ||
| map[string]interface{}{"provider": "github"}, | ||
| ) |
There was a problem hiding this comment.
OAuth callback routes are unauthenticated in the generated wrapper, so ctx.Request().Context() likely lacks analytics.UserIDKey when calling Enqueue. That results in an empty DistinctId and the login event being dropped. Consider creating a derived context with analytics.UserIDKey set to user.ID for the analytics call.
| func (a *AdminServiceImpl) PutWorkflow(ctx context.Context, req *contracts.CreateWorkflowVersionRequest) (*contracts.CreateWorkflowVersionResponse, error) { | ||
| tenant := ctx.Value("tenant").(*sqlcv1.Tenant) | ||
| tenantId := tenant.ID | ||
| a.analytics.Count(ctx, analytics.Workflow, analytics.Create, putWorkflowFeatureFlags(req)) | ||
|
|
There was a problem hiding this comment.
This method currently emits both Count(workflow:create, feature-flags) and Enqueue(workflow:create, workflow_id) for the same operation, which will double-count workflow creations in PostHog. Also, workflow CRUD is typically low-throughput, so aggregation via Count is likely unnecessary here. Consider consolidating into a single Enqueue call and include the feature-flag properties there.
| func (a *AdminServiceImpl) CancelTasks(ctx context.Context, req *contracts.CancelTasksRequest) (*contracts.CancelTasksResponse, error) { | ||
| tenant := ctx.Value("tenant").(*sqlcv1.Tenant) | ||
| // FIXME-ANALYTICS: Count number of tasks cancelled | ||
| a.analytics.Count(ctx, analytics.TaskRun, analytics.Cancel) | ||
|
|
There was a problem hiding this comment.
CancelTasks can cancel many tasks, but the instrumentation increments the counter only once per request. This undercounts cancellations (the FIXME suggests this is known). Consider counting based on the actual number of tasks cancelled (e.g., derived from tasksToCancel / filter results) rather than per-request.
| func (a *AdminServiceImpl) ReplayTasks(ctx context.Context, req *contracts.ReplayTasksRequest) (*contracts.ReplayTasksResponse, error) { | ||
| tenant := ctx.Value("tenant").(*sqlcv1.Tenant) | ||
| // FIXME-ANALYTICS: Count number of tasks replayed | ||
| a.analytics.Count(ctx, analytics.TaskRun, analytics.Replay) | ||
|
|
There was a problem hiding this comment.
ReplayTasks can replay many tasks, but the instrumentation increments the counter only once per request. This undercounts replays (the FIXME suggests this is known). Consider counting based on the actual number of tasks replayed (e.g., derived from filter results) rather than per-request.
| cleanup = func() error { | ||
| log.Printf("cleaning up server config") | ||
|
|
||
| if err := cleanupSchedulingPoolV1(); err != nil { | ||
| return fmt.Errorf("error cleaning up scheduling pool (v1): %w", err) | ||
| } | ||
|
|
||
| if err := cleanup1(); err != nil { | ||
| return fmt.Errorf("error cleaning up rabbitmq: %w", err) | ||
| } | ||
|
|
||
| if closeErr := analyticsEmitter.Close(); closeErr != nil { | ||
| l.Error().Err(closeErr).Msg("error closing analytics emitter") | ||
| } |
There was a problem hiding this comment.
In this cleanup function, returning early on cleanupSchedulingPoolV1() / cleanup1() errors will skip analyticsEmitter.Close(), potentially leaving the aggregator goroutine running and dropping buffered analytics on shutdown. Consider ensuring Close() is always attempted (e.g., defer it at the start of the cleanup or aggregate errors).
| t.config.Analytics.Enqueue( | ||
| ctx.Request().Context(), | ||
| analytics.Tenant, analytics.Create, | ||
| tenantId.String(), | ||
| map[string]interface{}{ | ||
| "name": tenant.Name, | ||
| "slug": tenant.Slug, | ||
| }, | ||
| ) |
There was a problem hiding this comment.
TenantCreate is the one place where the tenant is being created, so the request context won’t already have analytics.TenantIDKey set. As a result, this tenant:create event won’t be associated with the tenant group in PostHog. Consider deriving a context with analytics.TenantIDKey = tenantId for the Enqueue call so the event is grouped to the newly created tenant.
| // Props builds a property map from variadic key-value pairs, keeping all | ||
| // non-nil values regardless of type. Keys must be strings; non-string keys | ||
| // are skipped. Nil values are omitted. | ||
| func Props(kvs ...interface{}) map[string]interface{} { |
There was a problem hiding this comment.
can we use a more informative type than interface{} here?
There was a problem hiding this comment.
blocking because this makes me very nervous, both that we'll blow up our Posthog budget, and that we'll have a bunch of performance degradation, memory leak risk, etc. on the API, and, more importantly, on the engine from this. I think we need to discuss this more, and I also still feel pretty strongly that e.g. tracking counts of things like task runs, stream events, etc. in Posthog is not the right place to be putting them, but it's fine IMO if we want to make dashboards and stuff from them 🤷
mrkaye97
left a comment
There was a problem hiding this comment.
discussed sync, let's give it a shot
(still one or two comments to address but wanted to unblock)
Benchmark resultsCompared against |
Description
Improves analytics eventing consistency throughout applicaiton surface.
Type of change