[Portal] Add Create Application baseline metrics#5806
Conversation
| }, | ||
| } | ||
|
|
||
| var cmdAnalyticPosthogFirstAuth = &cobra.Command{ |
There was a problem hiding this comment.
@fungc-io
Instead of adding extra command which require a cronjob to run it, can we simply send an event at the moment the first auth occcurs?
There was a problem hiding this comment.
Done — reworked in the latest commits. The batch subcommand + cronjob is gone; application.first_auth is now sent at the moment the first auth occurs:
- Added
FirstAuthSink(pkg/lib/analytic/first_auth_sink.go), a newevent.Sinkregistered inevent.NewService. It reacts touser.authenticatedandm2m.token.created, takingapp_id/client_idfrom the event context. - Once-per-client is enforced with
SETNXon the analytic Redis (app:<app_id>:posthog-first-auth:<client_id>, 90-day TTL). The event uuid stays a deterministic UUIDv5 of(app_id, client_id), so PostHog dedupes even if the Redis key is ever lost. - Delivery is fire-and-forget (detached context + goroutine, same pattern as non-blocking webhook delivery), so the auth response never waits on PostHog. Missing credentials → the sink is a no-op.
- Removed
analytic posthog first-auth,ForwardFirstAuthEvents, and the audit-DB queryGetFirstAuthTimeByClientID. No cronjob needed.
One deployment note: the main server now needs ANALYTIC_POSTHOG_ENDPOINT / ANALYTIC_POSTHOG_APIKEY set (previously only the portal had PostHog credentials), since the sink runs where auth happens.
Expose PostHog credentials to server binaries via EnvironmentConfig.Analytic (ANALYTIC_POSTHOG_ENDPOINT / ANALYTIC_POSTHOG_APIKEY), extract a shared analytic.NewPosthogCredentials, and provide *config.AnalyticConfig in DI. ref DEV-2392
Add FirstAuthSink, an event.Sink that forwards a single application.first_auth event to PostHog the first time each OAuth client authenticates. Dedup is a SETNX on the analytic Redis; delivery is a detached fire-and-forget goroutine so the auth response is never blocked. Extract buildFirstAuthEvent so the deterministic-uuid event shape is shared. ref DEV-2392
Add the sink as a parameter of event.NewService and register analytic.FirstAuthSinkDependencySet in the common dependency set. Regenerate wire for the auth, admin, resolver, and redisqueue graphs. ref DEV-2392
The real-time FirstAuthSink replaces the batch job, so drop the 'analytic posthog first-auth' subcommand, PosthogIntegration.ForwardFirstAuthEvents, and the audit-DB query GetFirstAuthTimeByClientID. No cronjob is needed. ref DEV-2392
These graphs also construct event.NewService and must pass the new FirstAuthSink argument. Companion to the event-service wiring commit. ref DEV-2392
| logger := FirstAuthSinkLogger.GetLogger(ctx) | ||
|
|
||
| // Only successful-auth events count as a "first auth". | ||
| if e.Type != nonblocking.UserAuthenticated && e.Type != nonblocking.M2MTokenCreated { |
There was a problem hiding this comment.
Should this check user.created too?
| func (s *FirstAuthSink) markFirstAuth(ctx context.Context, appID string, clientID string, at time.Time) (bool, error) { | ||
| key := firstAuthDedupKey(appID, clientID) | ||
| var keyWasSet bool | ||
| err := s.AnalyticRedis.WithConnContext(ctx, func(ctx context.Context, conn redis.Redis_6_0_Cmdable) error { | ||
| var err error | ||
| keyWasSet, err = conn.SetNX(ctx, key, at.UTC().Format(time.RFC3339), firstAuthDedupTTL).Result() | ||
| return err | ||
| }) | ||
| if err != nil { | ||
| return false, err | ||
| } | ||
| return keyWasSet, nil | ||
| } | ||
|
|
||
| func firstAuthDedupKey(appID string, clientID string) string { | ||
| return fmt.Sprintf("app:%s:posthog-first-auth:%s", appID, clientID) | ||
| } |
There was a problem hiding this comment.
Is it possible to check audit db instead of storing a new key in redis?
The current implementation may send duplicated events if firstAuthDedupTTL passed
There was a problem hiding this comment.
But actually if retention period of audit log passed there will also be duplicated events. Maybe just keep the current implementation then.
Summary
Baseline usage instrumentation for the Create Application flow, so we can measure funnel drop-off, activation, and time-to-integration. This is PR-1 of 2: it instruments the current (legacy) Create Application screen and stands up the north-star pipeline, so a clean "before" baseline accrues before the framework-first wizard ships (PR-2 will emit the same events tagged
wizard_version: "framework_first").No auth-server / token-path changes — the north-star is derived from existing audit data.
What it adds
useCapture()GTM hook, all taggedwizard_version: "legacy":createApplication.viewed(once per mount),createApplication.selected-type(application_type),createApplication.created(client_id,application_type).application.first_authforwarder — derives first successful auth per client from existing_audit_logrows (user.authenticated/m2m.token.created, which already carryclient_id) and forwards them to PostHog:AuditDBReadStore.GetFirstAuthTimeByClientID(tenant-scoped, parameterized).PosthogIntegration.ForwardFirstAuthEvents— idempotent via a deterministic UUIDv5 per(app_id, client_id); eventtimestamp= the real audit auth time (so batch lag never inflates time-to-integration).portal analytic posthog first-auth, mirroring the existingposthog group/userjobs.docs/analytics/create-application-wizard-metrics.md— the two PostHog insights (person drop-off funnel;client_idactivation funnel + HogQL) and caveats.Deployment contract
Schedule
portal analytic posthog first-authon a cron well under 35 days (daily/hourly). The forwarder scans a 35-day lookback (≤ the 90-day audit retention); the interval must stay under the lookback or first-auths occurring in a gap are missed.Verification
go build ./...,go vet, andgo test ./pkg/lib/analytic/...pass (incl. a determinism test for the event uuid).npm run typecheck/ lint / fmt clean.viewedfires exactly once per mount.WHERE app_id = ?) and SQL parameterization confirmed; the audit query is covered by the existing_audit_log(app_id, activity_type, created_at)index.Known follow-ups (non-blocking)
count()ofapplication.first_authcan double-count clients active beyond the 35-day lookback (same uuid, later timestamp); documented — always aggregate withmin()/ funnel first-match (as the insights do).useCapturefallback instability also exists atGetStartedScreen; better fixed centrally ingtm_v2.tsxthan per call site.