Files
dd0c/products/01-llm-cost-router/tests/analytics/analytics.spec.ts
Max Mayfield 03bfe931fc Implement review remediation + PLG analytics SDK
- All 6 test architectures patched with Section 11 addendums
- P5 (cost) fully rewritten from 232 to ~600 lines
- PLG brainstorm + party mode advisory board results
- Analytics SDK v2 (PostHog Cloud, Zod strict, Lambda-safe)
- Analytics tests v2 (safeParse, no , no timestamp, no PII)
- Addresses all Gemini review findings across P1-P6
2026-03-01 01:42:49 +00:00

205 lines
6.7 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { Analytics, EventSchema, EventName } from '../../src/analytics';
import { PostHog } from 'posthog-node';
vi.mock('posthog-node');
describe('Analytics SDK (PostHog Cloud — v2 Post-Review)', () => {
let analytics: Analytics;
let mockPostHog: vi.Mocked<PostHog>;
beforeEach(() => {
vi.clearAllMocks();
mockPostHog = new PostHog('phc_test_key', { host: 'https://us.i.posthog.com' }) as any;
analytics = new Analytics(mockPostHog);
});
// ── Schema Validation (Zod) ──────────────────────────────
describe('Event Taxonomy Validation', () => {
it('accepts valid account.signup.completed event', () => {
const event = {
name: EventName.SignupCompleted,
tenant_id: 'tenant-123',
product: 'route' as const,
properties: { method: 'github_sso' },
};
expect(() => EventSchema.parse(event)).not.toThrow();
});
it('rejects events missing tenant_id', () => {
const event = {
name: EventName.SignupCompleted,
product: 'route',
properties: { method: 'email' },
};
expect(() => EventSchema.parse(event as any)).toThrow(/tenant_id/);
});
it('accepts valid activation event', () => {
const event = {
name: EventName.FirstDollarSaved,
tenant_id: 'tenant-123',
product: 'route' as const,
properties: { savings_amount: 1.50 },
};
expect(() => EventSchema.parse(event)).not.toThrow();
});
it('accepts valid upgrade event', () => {
const event = {
name: EventName.UpgradeCompleted,
tenant_id: 'tenant-123',
product: 'route' as const,
properties: { plan: 'pro', mrr_increase: 49 },
};
expect(() => EventSchema.parse(event)).not.toThrow();
});
});
// ── track() Behavior ─────────────────────────────────────
describe('track()', () => {
it('captures valid events via PostHog client', () => {
const result = analytics.track({
name: EventName.SignupCompleted,
tenant_id: 'tenant-123',
product: 'route',
properties: { method: 'email' },
});
expect(result).toBe(true);
expect(mockPostHog.capture).toHaveBeenCalledWith(
expect.objectContaining({
distinctId: 'tenant-123',
event: 'account.signup.completed',
properties: expect.objectContaining({
product: 'route',
method: 'email',
}),
})
);
});
it('does NOT include $set in track calls (use identify instead)', () => {
analytics.track({
name: EventName.SignupCompleted,
tenant_id: 'tenant-123',
product: 'route',
properties: { method: 'github_sso' },
});
const captureCall = mockPostHog.capture.mock.calls[0][0];
expect(captureCall.properties).not.toHaveProperty('$set');
});
it('does NOT pass timestamp (let PostHog handle it to avoid clock skew)', () => {
analytics.track({
name: EventName.SignupCompleted,
tenant_id: 'tenant-123',
product: 'route',
properties: { method: 'email' },
});
const captureCall = mockPostHog.capture.mock.calls[0][0];
expect(captureCall).not.toHaveProperty('timestamp');
});
it('returns false and does NOT call PostHog if base validation fails', () => {
const result = analytics.track({
name: 'invalid.event' as any,
tenant_id: 'tenant-123',
product: 'route',
});
expect(result).toBe(false);
expect(mockPostHog.capture).not.toHaveBeenCalled();
});
it('returns false if per-event property validation fails (strict schema)', () => {
const result = analytics.track({
name: EventName.SignupCompleted,
tenant_id: 'tenant-123',
product: 'route',
properties: { method: 'invalid_method' }, // Not in enum
});
expect(result).toBe(false);
expect(mockPostHog.capture).not.toHaveBeenCalled();
});
it('rejects unknown properties (strict mode — no PII loophole)', () => {
const result = analytics.track({
name: EventName.SignupCompleted,
tenant_id: 'tenant-123',
product: 'route',
properties: { method: 'email', email: 'user@example.com' }, // PII leak attempt
});
expect(result).toBe(false);
expect(mockPostHog.capture).not.toHaveBeenCalled();
});
it('does NOT flush after each track call (Lambda batching)', () => {
analytics.track({
name: EventName.SignupCompleted,
tenant_id: 'tenant-123',
product: 'route',
properties: { method: 'email' },
});
expect(mockPostHog.flushAsync).not.toHaveBeenCalled();
});
});
// ── identify() ───────────────────────────────────────────
describe('identify()', () => {
it('calls PostHog identify with tenant_id as distinctId', () => {
analytics.identify('tenant-123', { company: 'Acme' });
expect(mockPostHog.identify).toHaveBeenCalledWith(
expect.objectContaining({
distinctId: 'tenant-123',
properties: expect.objectContaining({
tenant_id: 'tenant-123',
company: 'Acme',
}),
})
);
});
});
// ── flush() ──────────────────────────────────────────────
describe('flush()', () => {
it('calls flushAsync on the PostHog client', async () => {
await analytics.flush();
expect(mockPostHog.flushAsync).toHaveBeenCalledTimes(1);
});
});
// ── NoOp Client ──────────────────────────────────────────
describe('NoOp Client (missing API key)', () => {
it('does not throw when tracking without API key', () => {
const noopAnalytics = new Analytics(); // No client, no env var
const result = noopAnalytics.track({
name: EventName.SignupCompleted,
tenant_id: 'tenant-123',
product: 'route',
properties: { method: 'email' },
});
expect(result).toBe(true); // NoOp accepts everything silently
});
});
// ── Session Replay ───────────────────────────────────────
describe('Security', () => {
it('session replay is disabled', () => {
expect(analytics.isSessionReplayEnabled).toBe(false);
});
});
});