Skip to content

Commit f26d978

Browse files
gewenyu99claude
andcommitted
feat(analytics): identify the user so feature flags can target by email (#620)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6c2318d commit f26d978

2 files changed

Lines changed: 37 additions & 24 deletions

File tree

src/lib/agent/agent-runner.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,9 @@ async function bootstrapProgram(
349349
getUI().setRoleAtOrganization(roleAtOrganization);
350350
getUI().setApiUser(user);
351351

352+
// Identify the user (email, name) before evaluating flags, so flags can target
353+
// the individual user and not just $app_name.
354+
if (user) analytics.identifyUser(user);
352355
analytics.setGroups(groupsFromUser(user, host));
353356

354357
// 4.5. AI opt-in enforcement. Parks here while AiOptInRequiredScreen is

src/utils/analytics.ts

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { WizardSession } from '@lib/wizard-session';
88
import type { ApiUser } from '@lib/api';
99
import { v4 as uuidv4 } from 'uuid';
1010
import { IS_PRODUCTION_BUILD } from '@env';
11-
import { debug } from './debug';
11+
import { debug, logToFile } from './debug';
1212

1313
/**
1414
* Extract a standard property bag from the current session.
@@ -58,6 +58,7 @@ export class Analytics {
5858
private appName = 'wizard';
5959
private activeFlags: Record<string, string> | null = null;
6060
private groups: Record<string, string> = {};
61+
private personProperties: Record<string, string> = {};
6162

6263
constructor() {
6364
this.client = new PostHog(ANALYTICS_POSTHOG_PUBLIC_PROJECT_WRITE_KEY, {
@@ -107,10 +108,12 @@ export class Analytics {
107108
}
108109

109110
/**
110-
* Associate the run with the logged-in user, once per id: identify them
111-
* (email, name), then alias the run's anonymous id onto the identified
112-
* person so pre-login events merge in. Alias only ever fires after
113-
* identification.
111+
* Associate the run with the logged-in user, once per id. Identifies them
112+
* (email, name) and records those person properties so events carry them and
113+
* feature flags can target the individual user — without the email here the
114+
* wizard only sends `$app_name`, so email-targeted flags never match. Opens
115+
* the analytics session on first login, then aliases the run's anonymous id
116+
* onto the identified person so pre-login events merge in.
114117
*/
115118
identifyUser(user: ApiUser) {
116119
const distinctId = user.distinct_id;
@@ -127,25 +130,28 @@ export class Analytics {
127130
this.sessionId = uuidv4();
128131
this.tags.$session_id = this.sessionId;
129132
}
130-
this.client.identify({
131-
distinctId,
132-
properties: {
133-
$set: {
134-
...(user.email ? { email: user.email } : {}),
135-
...(user.first_name || user.last_name
136-
? {
137-
name: [user.first_name, user.last_name]
138-
.filter(Boolean)
139-
.join(' '),
140-
}
141-
: {}),
142-
},
143-
},
144-
});
133+
const props: Record<string, string> = {};
134+
if (user.email) props.email = user.email;
135+
const name = [user.first_name, user.last_name]
136+
.filter(Boolean)
137+
.join(' ')
138+
.trim();
139+
if (name) props.name = name;
140+
this.personProperties = props;
141+
this.client.identify({ distinctId, properties: { $set: props } });
145142
this.client.alias({
146143
distinctId,
147144
alias: this.anonymousId,
148145
});
146+
// The flag snapshot is per identity. Anything evaluated before login (the
147+
// intro screen reads the tools-menu flag) was anonymous — drop it so the
148+
// next read re-evaluates as this user.
149+
this.activeFlags = null;
150+
}
151+
152+
/** Person properties sent with flag evaluation: app name plus the user's. */
153+
private flagPersonProperties(): Record<string, string> {
154+
return { $app_name: this.appName, ...this.personProperties };
149155
}
150156

151157
setTag(key: string, value: string | boolean | number | null | undefined) {
@@ -198,9 +204,7 @@ export class Analytics {
198204
const distinctId = this.distinctId ?? this.anonymousId;
199205
return await this.client.getFeatureFlag(flagKey, distinctId, {
200206
sendFeatureFlagEvents: true,
201-
personProperties: {
202-
$app_name: this.appName,
203-
},
207+
personProperties: this.flagPersonProperties(),
204208
});
205209
} catch (error) {
206210
debug('Failed to get feature flag:', flagKey, error);
@@ -219,8 +223,13 @@ export class Analytics {
219223
}
220224
try {
221225
const distinctId = this.distinctId ?? this.anonymousId;
226+
logToFile('[flags] evaluating as', {
227+
distinctId,
228+
identified: this.distinctId !== undefined,
229+
personProperties: this.flagPersonProperties(),
230+
});
222231
const result = await this.client.getAllFlagsAndPayloads(distinctId, {
223-
personProperties: { $app_name: this.appName },
232+
personProperties: this.flagPersonProperties(),
224233
});
225234
const flags = result.featureFlags ?? {};
226235
const out: Record<string, string> = {};
@@ -229,6 +238,7 @@ export class Analytics {
229238
out[key] = typeof value === 'boolean' ? String(value) : String(value);
230239
}
231240
this.activeFlags = out;
241+
logToFile('[flags] evaluated', out);
232242
return out;
233243
} catch (error) {
234244
debug('Failed to get all feature flags:', error);

0 commit comments

Comments
 (0)