Skip to content

[Portal] Add Create Application baseline metrics#5806

Open
fungc-io wants to merge 15 commits into
authgear:mainfrom
fungc-io:fungc-io/create-application-baseline-metrics
Open

[Portal] Add Create Application baseline metrics#5806
fungc-io wants to merge 15 commits into
authgear:mainfrom
fungc-io:fungc-io/create-application-baseline-metrics

Conversation

@fungc-io

@fungc-io fungc-io commented Jul 9, 2026

Copy link
Copy Markdown
Member

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

  • Frontend funnel events on the legacy Create Application screen via the existing useCapture() GTM hook, all tagged wizard_version: "legacy":
    • createApplication.viewed (once per mount), createApplication.selected-type (application_type), createApplication.created (client_id, application_type).
  • application.first_auth forwarder — derives first successful auth per client from existing _audit_log rows (user.authenticated / m2m.token.created, which already carry client_id) and forwards them to PostHog:
    • AuditDBReadStore.GetFirstAuthTimeByClientID (tenant-scoped, parameterized).
    • PosthogIntegration.ForwardFirstAuthEvents — idempotent via a deterministic UUIDv5 per (app_id, client_id); event timestamp = the real audit auth time (so batch lag never inflates time-to-integration).
    • New batch subcommand portal analytic posthog first-auth, mirroring the existing posthog group/user jobs.
  • Report doc docs/analytics/create-application-wizard-metrics.md — the two PostHog insights (person drop-off funnel; client_id activation funnel + HogQL) and caveats.

Deployment contract

Schedule portal analytic posthog first-auth on 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, and go test ./pkg/lib/analytic/... pass (incl. a determinism test for the event uuid).
  • Portal npm run typecheck / lint / fmt clean.
  • The three frontend events were browser-verified firing with correct payloads, and viewed fires exactly once per mount.
  • Tenant scoping (auto-injected 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)

  • Raw count() of application.first_auth can double-count clients active beyond the 35-day lookback (same uuid, later timestamp); documented — always aggregate with min() / funnel first-match (as the insights do).
  • The dev-only useCapture fallback instability also exists at GetStartedScreen; better fixed centrally in gtm_v2.tsx than per call site.

Comment thread cmd/portal/cmd/cmdanalytic/posthog.go Outdated
},
}

var cmdAnalyticPosthogFirstAuth = &cobra.Command{

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.

@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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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 new event.Sink registered in event.NewService. It reacts to user.authenticated and m2m.token.created, taking app_id / client_id from the event context.
  • Once-per-client is enforced with SETNX on 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 query GetFirstAuthTimeByClientID. 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.

fungc-io added 8 commits July 21, 2026 15:52
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
@fungc-io
fungc-io requested a review from tung2744 July 21, 2026 16:49
logger := FirstAuthSinkLogger.GetLogger(ctx)

// Only successful-auth events count as a "first auth".
if e.Type != nonblocking.UserAuthenticated && e.Type != nonblocking.M2MTokenCreated {

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.

Should this check user.created too?

Comment on lines +97 to +113
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)
}

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.

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

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.

But actually if retention period of audit log passed there will also be duplicated events. Maybe just keep the current implementation then.

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.

2 participants