diff --git a/src/lib/agent/agent-runner.ts b/src/lib/agent/agent-runner.ts index 07a89c0c..a08320ed 100644 --- a/src/lib/agent/agent-runner.ts +++ b/src/lib/agent/agent-runner.ts @@ -349,6 +349,9 @@ async function bootstrapProgram( getUI().setRoleAtOrganization(roleAtOrganization); getUI().setApiUser(user); + // Identify the user (email, name) before evaluating flags, so flags can target + // the individual user and not just $app_name. + if (user) analytics.identifyUser(user); analytics.setGroups(groupsFromUser(user, host)); // 4.5. AI opt-in enforcement. Parks here while AiOptInRequiredScreen is diff --git a/src/utils/analytics.ts b/src/utils/analytics.ts index 90ab0cd6..2cfadd3f 100644 --- a/src/utils/analytics.ts +++ b/src/utils/analytics.ts @@ -8,7 +8,7 @@ import type { WizardSession } from '@lib/wizard-session'; import type { ApiUser } from '@lib/api'; import { v4 as uuidv4 } from 'uuid'; import { IS_PRODUCTION_BUILD } from '@env'; -import { debug } from './debug'; +import { debug, logToFile } from './debug'; /** * Extract a standard property bag from the current session. @@ -58,6 +58,7 @@ export class Analytics { private appName = 'wizard'; private activeFlags: Record | null = null; private groups: Record = {}; + private personProperties: Record = {}; constructor() { this.client = new PostHog(ANALYTICS_POSTHOG_PUBLIC_PROJECT_WRITE_KEY, { @@ -107,10 +108,12 @@ export class Analytics { } /** - * Associate the run with the logged-in user, once per id: identify them - * (email, name), then alias the run's anonymous id onto the identified - * person so pre-login events merge in. Alias only ever fires after - * identification. + * Associate the run with the logged-in user, once per id. Identifies them + * (email, name) and records those person properties so events carry them and + * feature flags can target the individual user — without the email here the + * wizard only sends `$app_name`, so email-targeted flags never match. Opens + * the analytics session on first login, then aliases the run's anonymous id + * onto the identified person so pre-login events merge in. */ identifyUser(user: ApiUser) { const distinctId = user.distinct_id; @@ -127,25 +130,28 @@ export class Analytics { this.sessionId = uuidv4(); this.tags.$session_id = this.sessionId; } - this.client.identify({ - distinctId, - properties: { - $set: { - ...(user.email ? { email: user.email } : {}), - ...(user.first_name || user.last_name - ? { - name: [user.first_name, user.last_name] - .filter(Boolean) - .join(' '), - } - : {}), - }, - }, - }); + const props: Record = {}; + if (user.email) props.email = user.email; + const name = [user.first_name, user.last_name] + .filter(Boolean) + .join(' ') + .trim(); + if (name) props.name = name; + this.personProperties = props; + this.client.identify({ distinctId, properties: { $set: props } }); this.client.alias({ distinctId, alias: this.anonymousId, }); + // The flag snapshot is per identity. Anything evaluated before login (the + // intro screen reads the tools-menu flag) was anonymous — drop it so the + // next read re-evaluates as this user. + this.activeFlags = null; + } + + /** Person properties sent with flag evaluation: app name plus the user's. */ + private flagPersonProperties(): Record { + return { $app_name: this.appName, ...this.personProperties }; } setTag(key: string, value: string | boolean | number | null | undefined) { @@ -188,9 +194,7 @@ export class Analytics { const distinctId = this.distinctId ?? this.anonymousId; return await this.client.getFeatureFlag(flagKey, distinctId, { sendFeatureFlagEvents: true, - personProperties: { - $app_name: this.appName, - }, + personProperties: this.flagPersonProperties(), }); } catch (error) { debug('Failed to get feature flag:', flagKey, error); @@ -209,8 +213,13 @@ export class Analytics { } try { const distinctId = this.distinctId ?? this.anonymousId; + logToFile('[flags] evaluating as', { + distinctId, + identified: this.distinctId !== undefined, + personProperties: this.flagPersonProperties(), + }); const result = await this.client.getAllFlagsAndPayloads(distinctId, { - personProperties: { $app_name: this.appName }, + personProperties: this.flagPersonProperties(), }); const flags = result.featureFlags ?? {}; const out: Record = {}; @@ -219,6 +228,7 @@ export class Analytics { out[key] = typeof value === 'boolean' ? String(value) : String(value); } this.activeFlags = out; + logToFile('[flags] evaluated', out); return out; } catch (error) { debug('Failed to get all feature flags:', error);