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
This commit is contained in:
204
products/01-llm-cost-router/tests/analytics/analytics.spec.ts
Normal file
204
products/01-llm-cost-router/tests/analytics/analytics.spec.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user