Skip to content

Commit d306bbf

Browse files
gewenyu99claude
andcommitted
feat(analytics): identify the user so feature flags can target by email
identifyUser sets the distinct id and person properties (email, name) before flag evaluation, and getFeatureFlag/getAllFlagsForWizard now send them. Previously the wizard sent only $app_name, so email-targeted flags never matched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2c61397 commit d306bbf

2 files changed

Lines changed: 41 additions & 5 deletions

File tree

src/lib/agent/agent-runner.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,9 @@ async function bootstrapProgram(
327327
getUI().setRoleAtOrganization(roleAtOrganization);
328328
getUI().setApiUser(user);
329329

330+
// Identify the user (email, name) before evaluating flags, so flags can target
331+
// the individual user and not just $app_name.
332+
if (user) analytics.identifyUser(user);
330333
analytics.setGroups(groupsFromUser(user, host));
331334

332335
// Feature flags, variant metadata, and MCP url. Both arms need these, and the

src/utils/analytics.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
import type { WizardSession } from '@lib/wizard-session';
88
import type { ApiUser } from '@lib/api';
99
import { v4 as uuidv4 } from 'uuid';
10-
import { debug } from './debug';
10+
import { debug, logToFile } from './debug';
1111

1212
/**
1313
* Extract a standard property bag from the current session.
@@ -55,6 +55,7 @@ export class Analytics {
5555
private appName = 'wizard';
5656
private activeFlags: Record<string, string> | null = null;
5757
private groups: Record<string, string> = {};
58+
private personProperties: Record<string, string> = {};
5859

5960
constructor() {
6061
this.client = new PostHog(ANALYTICS_POSTHOG_PUBLIC_PROJECT_WRITE_KEY, {
@@ -85,6 +86,34 @@ export class Analytics {
8586
});
8687
}
8788

89+
/**
90+
* Identify the authenticated user. Sets the distinct id and records their
91+
* person properties (email, name) so events carry them and feature flags can
92+
* target the individual user. Without the email here, the wizard only sends
93+
* `$app_name`, so flags targeted by email never match.
94+
*/
95+
identifyUser(user: ApiUser) {
96+
this.setDistinctId(user.distinct_id);
97+
const props: Record<string, string> = {};
98+
if (user.email) props.email = user.email;
99+
const name = [user.first_name, user.last_name]
100+
.filter(Boolean)
101+
.join(' ')
102+
.trim();
103+
if (name) props.name = name;
104+
this.personProperties = props;
105+
this.client.identify({ distinctId: user.distinct_id, properties: props });
106+
// The flag snapshot is per identity. Anything evaluated before login (the
107+
// intro screen reads the tools-menu flag) was anonymous — drop it so the
108+
// next read re-evaluates as this user.
109+
this.activeFlags = null;
110+
}
111+
112+
/** Person properties sent with flag evaluation: app name plus the user's. */
113+
private flagPersonProperties(): Record<string, string> {
114+
return { $app_name: this.appName, ...this.personProperties };
115+
}
116+
88117
setTag(key: string, value: string | boolean | number | null | undefined) {
89118
this.tags[key] = value;
90119
}
@@ -125,9 +154,7 @@ export class Analytics {
125154
const distinctId = this.distinctId ?? this.anonymousId;
126155
return await this.client.getFeatureFlag(flagKey, distinctId, {
127156
sendFeatureFlagEvents: true,
128-
personProperties: {
129-
$app_name: this.appName,
130-
},
157+
personProperties: this.flagPersonProperties(),
131158
});
132159
} catch (error) {
133160
debug('Failed to get feature flag:', flagKey, error);
@@ -146,8 +173,13 @@ export class Analytics {
146173
}
147174
try {
148175
const distinctId = this.distinctId ?? this.anonymousId;
176+
logToFile('[flags] evaluating as', {
177+
distinctId,
178+
identified: this.distinctId !== undefined,
179+
personProperties: this.flagPersonProperties(),
180+
});
149181
const result = await this.client.getAllFlagsAndPayloads(distinctId, {
150-
personProperties: { $app_name: this.appName },
182+
personProperties: this.flagPersonProperties(),
151183
});
152184
const flags = result.featureFlags ?? {};
153185
const out: Record<string, string> = {};
@@ -156,6 +188,7 @@ export class Analytics {
156188
out[key] = typeof value === 'boolean' ? String(value) : String(value);
157189
}
158190
this.activeFlags = out;
191+
logToFile('[flags] evaluated', out);
159192
return out;
160193
} catch (error) {
161194
debug('Failed to get all feature flags:', error);

0 commit comments

Comments
 (0)