Skip to content

Commit ca9d1eb

Browse files
authored
Merge pull request #143 from InsForge/feat/posthog-funnel-analytics
feat(posthog): track cli_posthog_invoked on posthog setup
2 parents 3f8843d + e54a6e4 commit ca9d1eb

3 files changed

Lines changed: 59 additions & 0 deletions

File tree

src/commands/posthog/setup.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ const configMock = vi.hoisted(() => ({
1414
}));
1515
vi.mock('../../lib/config.js', () => configMock);
1616

17+
const analyticsMock = vi.hoisted(() => ({
18+
trackPosthog: vi.fn(),
19+
shutdownAnalytics: vi.fn(async () => {}),
20+
}));
21+
vi.mock('../../lib/analytics.js', () => analyticsMock);
22+
1723
vi.mock('../../lib/prompts.js', () => ({ isInteractive: false }));
1824

1925
// `open` is loaded dynamically inside runConnectFlow; mock so the real browser
@@ -81,6 +87,8 @@ beforeEach(() => {
8187
outputMock.outputJson.mockReset();
8288
outputMock.outputSuccess.mockReset();
8389
clackNoteMock.mockReset();
90+
analyticsMock.trackPosthog.mockReset();
91+
analyticsMock.shutdownAnalytics.mockClear();
8492
configMock.getProjectConfig.mockReturnValue({ project_id: 'p1', project_name: 'Test Project' });
8593
configMock.getAccessToken.mockReturnValue('tok');
8694
});
@@ -175,4 +183,32 @@ describe('posthog setup', () => {
175183
expect(payload.wizardCommand).toMatch(/^npx(\.cmd)? -y @posthog\/wizard@latest$/);
176184
});
177185
});
186+
187+
describe('analytics', () => {
188+
it('fires cli_posthog_invoked with the project config and flushes on success', async () => {
189+
apiMock.startPosthogCliFlow.mockResolvedValue({ type: 'connected' });
190+
apiMock.fetchPosthogConnection.mockResolvedValue({
191+
kind: 'connected',
192+
connection: { apiKey: 'phc_', host: 'h', posthogProjectId: '1' },
193+
});
194+
195+
await runSetup(['--skip-browser']);
196+
197+
expect(analyticsMock.trackPosthog).toHaveBeenCalledOnce();
198+
expect(analyticsMock.trackPosthog).toHaveBeenCalledWith(
199+
'setup',
200+
expect.objectContaining({ project_id: 'p1' }),
201+
);
202+
expect(analyticsMock.shutdownAnalytics).toHaveBeenCalledOnce();
203+
});
204+
205+
it('still flushes analytics when setup fails', async () => {
206+
apiMock.startPosthogCliFlow.mockResolvedValue({ type: 'connected' });
207+
apiMock.fetchPosthogConnection.mockResolvedValue({ kind: 'not-connected' });
208+
209+
await runSetup(['--skip-browser']);
210+
211+
expect(analyticsMock.shutdownAnalytics).toHaveBeenCalledOnce();
212+
});
213+
});
178214
});

src/commands/posthog/setup.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
startPosthogCliFlow,
1717
} from '../../lib/api/posthog.js';
1818
import { outputJson, outputSuccess } from '../../lib/output.js';
19+
import { trackPosthog, shutdownAnalytics } from '../../lib/analytics.js';
1920

2021
const POLL_INTERVAL_MS = 2000;
2122
const POLL_TIMEOUT_MS = 15 * 60 * 1000;
@@ -52,6 +53,8 @@ export function registerPosthogSetupCommand(program: Command): void {
5253
}
5354
} catch (err) {
5455
handleError(err, json);
56+
} finally {
57+
await shutdownAnalytics();
5558
}
5659
});
5760
}
@@ -83,6 +86,8 @@ async function runSetup(opts: RunSetupOpts): Promise<SetupResult> {
8386
throw new AuthError('Not logged in. Run `insforge login` first.');
8487
}
8588

89+
trackPosthog('setup', proj);
90+
8691
if (!opts.json) {
8792
clack.intro('PostHog setup');
8893
outputSuccess(`Linked to InsForge project: ${proj.project_name} (${proj.project_id})`);

src/lib/analytics.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,24 @@ export function trackPayments(
6161
});
6262
}
6363

64+
// Step 2 of the "dashboard connect → CLI posthog setup" funnel; pair with
65+
// backend `posthog_connect_started` joined on project_id.
66+
export function trackPosthog(
67+
subcommand: string,
68+
config: ProjectConfig,
69+
properties?: Record<string, unknown>,
70+
): void {
71+
captureEvent(config.project_id, 'cli_posthog_invoked', {
72+
subcommand,
73+
project_id: config.project_id,
74+
project_name: config.project_name,
75+
org_id: config.org_id,
76+
region: config.region,
77+
oss_mode: config.project_id === FAKE_PROJECT_ID,
78+
...properties,
79+
});
80+
}
81+
6482
// Config commands (apply/plan/export) operate against an OSS backend and may
6583
// run without a linked cloud project, so the ProjectConfig is optional.
6684
// Pure-OSS runs fall back to FAKE_PROJECT_ID as the distinct ID — same

0 commit comments

Comments
 (0)