Skip to content

Feat--consistent-analytics-events#3239

Merged
grutt merged 17 commits into
mainfrom
feat--consistent-analytics-events
Mar 12, 2026
Merged

Feat--consistent-analytics-events#3239
grutt merged 17 commits into
mainfrom
feat--consistent-analytics-events

Conversation

@grutt

@grutt grutt commented Mar 11, 2026

Copy link
Copy Markdown
Member

Description

Improves analytics eventing consistency throughout applicaiton surface.

Type of change

  • Chore (changes which are not directly related to any business logic)

Copilot AI review requested due to automatic review settings March 11, 2026 15:43
@vercel

vercel Bot commented Mar 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hatchet-docs Ready Ready Preview, Comment Mar 12, 2026 6:51pm

Request Review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 new Count aggregation path.
  • Adds an Aggregator to batch Count events and integrates it into the PostHog analytics emitter with lifecycle (Start/Close).
  • Instruments multiple gRPC/HTTP handlers to emit Enqueue/Count events and threads Analytics through 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.

Comment thread internal/services/dispatcher/server.go Outdated
Comment thread api/v1/server/authn/middleware.go
Comment thread internal/services/grpc/middleware/rate_limit.go Outdated
Comment thread pkg/config/loader/loader.go
Comment thread pkg/analytics/aggregating_test.go
@grutt grutt requested a review from Copilot March 11, 2026 17:43
…t-dev/hatchet into feat--consistent-analytics-events

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 the analytics assignment or streamEventBufferTimeout depending 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.

Comment thread pkg/config/loader/loader.go
Comment thread pkg/config/loader/loader.go
Comment thread internal/services/grpc/middleware/rate_limit.go
Comment thread internal/services/dispatcher/server.go Outdated
Comment thread pkg/analytics/aggregating.go Outdated
Comment thread pkg/analytics/aggregating_test.go

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +68 to +73
u.config.Analytics.Enqueue(
ctx.Request().Context(),
analytics.User, analytics.Login,
existingUser.ID.String(),
map[string]interface{}{"provider": "basic"},
)

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +101 to +106
u.config.Analytics.Enqueue(
ctx.Request().Context(),
analytics.User, analytics.Create,
user.ID.String(),
map[string]interface{}{"provider": "basic"},
)

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +58 to +63
u.config.Analytics.Enqueue(
ctx.Request().Context(),
analytics.User, analytics.Login,
user.ID.String(),
map[string]interface{}{"provider": "google"},
)

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +64 to +69
u.config.Analytics.Enqueue(
ctx.Request().Context(),
analytics.User, analytics.Login,
user.ID.String(),
map[string]interface{}{"provider": "github"},
)

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 636 to 640
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))

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 30 to 34
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)

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 178 to 182
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)

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 696 to +708
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")
}

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +108
t.config.Analytics.Enqueue(
ctx.Request().Context(),
analytics.Tenant, analytics.Create,
tenantId.String(),
map[string]interface{}{
"name": tenant.Name,
"slug": tenant.Slug,
},
)

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread pkg/analytics/analytics.go Outdated
Comment thread pkg/analytics/analytics.go Outdated
// 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{} {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we use a more informative type than interface{} here?

@mrkaye97 mrkaye97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 mrkaye97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

discussed sync, let's give it a shot

(still one or two comments to address but wanted to unblock)

@github-actions

github-actions Bot commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Benchmark results

goos: linux
goarch: amd64
pkg: github.com/hatchet-dev/hatchet/internal/msgqueue/rabbitmq
cpu: AMD Ryzen 9 7950X3D 16-Core Processor          
                              │ /tmp/new.txt │
                              │    sec/op    │
CompressPayloads_1x10KiB-8       76.99µ ± 3%
CompressPayloads_10x10KiB-8      906.5µ ± 4%
CompressPayloads_10x100KiB-8     10.97m ± 3%
CompressPayloads_Concurrent-8    61.91µ ± 4%
geomean                          466.6µ

                              │ /tmp/new.txt │
                              │     B/op     │
CompressPayloads_1x10KiB-8      11.08Ki ± 4%
CompressPayloads_10x10KiB-8     111.0Ki ± 1%
CompressPayloads_10x100KiB-8    2.916Mi ± 0%
CompressPayloads_Concurrent-8   54.34Ki ± 1%
geomean                         118.9Ki

                              │ /tmp/new.txt │
                              │  allocs/op   │
CompressPayloads_1x10KiB-8        5.000 ± 0%
CompressPayloads_10x10KiB-8       32.00 ± 0%
CompressPayloads_10x100KiB-8      63.00 ± 2%
CompressPayloads_Concurrent-8     17.00 ± 0%
geomean                           20.35

Compared against main (66b0d93)

@mrkaye97 mrkaye97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀

@grutt grutt merged commit 6419ad3 into main Mar 12, 2026
52 checks passed
@grutt grutt deleted the feat--consistent-analytics-events branch March 12, 2026 19:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants