Files
wehub-resource-sync 070959e133
landing-page-staging / Deploy landing page to staging (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 4s
visual-baseline / Capture visual baselines (push) Has been cancelled
bake-plugin-previews / Bake plugin previews (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:00:47 +08:00

276 lines
8.5 KiB
TypeScript

// @vitest-environment jsdom
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { recordAmrEntry } from '../../src/analytics/amr-attribution';
import {
readOnboardingProfile,
saveOnboardingProfile,
type OnboardingProfile,
} from '../../src/state/onboarding-profile';
vi.mock('../../src/analytics/client', () => ({
setAnalyticsPersonProperties: vi.fn(),
}));
import { setAnalyticsPersonProperties } from '../../src/analytics/client';
import {
bindSignedInUserAttributionPersonProperties,
setOnboardingAttributionPersonProperties,
} from '../../src/analytics/source-attribution';
describe('source attribution person properties', () => {
beforeEach(() => {
window.localStorage.clear();
document.cookie = 'od_attr=;path=/;max-age=0';
vi.mocked(setAnalyticsPersonProperties).mockClear();
});
it('sets onboarding profile fields as PostHog person properties', () => {
setOnboardingAttributionPersonProperties(
{
role: 'engineer',
orgSize: 'growth',
useCase: ['product', 'unknown', ''],
source: 'github',
},
new Date('2026-07-02T08:00:00.000Z'),
);
expect(setAnalyticsPersonProperties).toHaveBeenCalledWith({
od_role: 'engineer',
od_org_size: 'growth',
od_use_cases: ['product'],
od_onboarding_source: 'github',
od_source_resolved: 'github',
od_source_resolution: 'onboarding',
od_onboarding_at: '2026-07-02T08:00:00.000Z',
});
});
it('never persists raw "other" free-text to the stored profile', () => {
// Even if a caller supplies a free-text detail, it must never be written to
// the persisted attribution profile — only the enumerated bucket survives.
saveOnboardingProfile(
{ source: 'other', sourceOther: 'Design podcast' } as unknown as OnboardingProfile,
new Date('2026-07-02T08:00:00.000Z'),
);
const stored = readOnboardingProfile();
expect(stored).toMatchObject({ source: 'other' });
expect(stored).not.toHaveProperty('sourceOther');
});
it('never carries raw "other" free-text into analytics person properties', () => {
// Analytics profile state must stay free-text/PII-free: the enumerated
// bucket is bound, but the raw channel the user typed is never surfaced.
setOnboardingAttributionPersonProperties(
{ source: 'other', sourceOther: 'Design podcast' } as unknown as OnboardingProfile,
new Date('2026-07-02T08:00:00.000Z'),
);
const props = vi.mocked(setAnalyticsPersonProperties).mock.calls[0]?.[0] ?? {};
expect(props).toMatchObject({ od_onboarding_source: 'other' });
expect(props).not.toHaveProperty('od_onboarding_source_other');
});
it('binds a signed-in AMR user to the stored onboarding source', () => {
const onboardingCompletedAt = new Date('2026-07-01T07:00:00.000Z');
saveOnboardingProfile(
{
role: 'growth',
orgSize: 'startup',
useCase: ['marketing'],
source: 'social',
},
onboardingCompletedAt,
);
bindSignedInUserAttributionPersonProperties(
'usr_amr_42',
new Date('2026-07-02T08:30:00.000Z'),
);
expect(setAnalyticsPersonProperties).toHaveBeenCalledWith(
expect.objectContaining({
od_app_user_id: 'usr_amr_42',
od_source_bound_at: '2026-07-02T08:30:00.000Z',
od_source_resolved: 'social',
od_source_resolution: 'onboarding',
od_role: 'growth',
od_org_size: 'startup',
od_use_cases: ['marketing'],
od_onboarding_source: 'social',
od_onboarding_at: '2026-07-01T07:00:00.000Z',
}),
);
});
it('binds landing UTM attribution to the signed-in AMR user', () => {
writeLandingAttributionCookie({
s: 'twitter',
m: 'organic_social',
c: '202606_story',
ct: 'official',
tm: 'design_agent',
ref: 'x.com',
lp: '/stories/ikigai-one/',
t: Date.parse('2026-06-30T12:00:00.000Z'),
});
bindSignedInUserAttributionPersonProperties(
'usr_amr_42',
new Date('2026-07-02T08:30:00.000Z'),
);
expect(setAnalyticsPersonProperties).toHaveBeenCalledWith(
expect.objectContaining({
od_app_user_id: 'usr_amr_42',
od_source_bound_at: '2026-07-02T08:30:00.000Z',
od_utm_source: 'twitter',
od_utm_medium: 'organic_social',
od_utm_campaign: '202606_story',
od_utm_content: 'official',
od_utm_term: 'design_agent',
od_referrer: 'x.com',
od_landing_path: '/stories/ikigai-one/',
od_utm_first_touch_at: '2026-06-30T12:00:00.000Z',
od_source_resolved: 'twitter',
od_source_resolution: 'utm',
}),
);
});
it('keeps UTM and onboarding separate while resolving source from UTM first', () => {
writeLandingAttributionCookie({
s: 'linkedin',
m: 'paid_social',
c: 'launch',
ct: 'founder_post',
lp: '/download',
t: Date.parse('2026-06-30T12:00:00.000Z'),
});
saveOnboardingProfile(
{
role: 'founder',
orgSize: 'solo',
useCase: ['landing_pages'],
source: 'github',
},
new Date('2026-07-01T07:00:00.000Z'),
);
bindSignedInUserAttributionPersonProperties(
'usr_amr_42',
new Date('2026-07-02T08:30:00.000Z'),
);
expect(setAnalyticsPersonProperties).toHaveBeenCalledWith(
expect.objectContaining({
od_utm_source: 'linkedin',
od_utm_medium: 'paid_social',
od_utm_campaign: 'launch',
od_utm_content: 'founder_post',
od_landing_path: '/download',
od_utm_first_touch_at: '2026-06-30T12:00:00.000Z',
od_onboarding_source: 'github',
od_onboarding_at: '2026-07-01T07:00:00.000Z',
od_source_resolved: 'linkedin',
od_source_resolution: 'utm',
}),
);
});
it('falls back to referrer attribution when od_attr has no UTM source', () => {
writeLandingAttributionCookie({
ref: 'google.com',
lp: '/download',
t: Date.parse('2026-06-30T12:00:00.000Z'),
});
bindSignedInUserAttributionPersonProperties(
'usr_amr_42',
new Date('2026-07-02T08:30:00.000Z'),
);
expect(setAnalyticsPersonProperties).toHaveBeenCalledWith(
expect.objectContaining({
od_referrer: 'google.com',
od_landing_path: '/download',
od_utm_first_touch_at: '2026-06-30T12:00:00.000Z',
od_source_resolved: 'google.com',
od_source_resolution: 'referrer',
}),
);
});
it('ignores malformed landing attribution cookies and falls back to onboarding', () => {
document.cookie = 'od_attr=%7Bbad-json;path=/';
saveOnboardingProfile(
{
role: 'designer',
orgSize: 'startup',
useCase: ['product'],
source: 'friend',
},
new Date('2026-07-01T07:00:00.000Z'),
);
bindSignedInUserAttributionPersonProperties(
'usr_amr_42',
new Date('2026-07-02T08:30:00.000Z'),
);
expect(setAnalyticsPersonProperties).toHaveBeenCalledWith(
expect.objectContaining({
od_onboarding_source: 'friend',
od_source_resolved: 'friend',
od_source_resolution: 'onboarding',
}),
);
});
it('binds the stored AMR entry attribution alongside onboarding fields', () => {
saveOnboardingProfile(
{
role: 'growth',
orgSize: 'startup',
useCase: ['marketing'],
source: 'social',
},
new Date('2026-07-01T07:00:00.000Z'),
);
const track = vi.fn();
recordAmrEntry(
track,
'inline_model_switcher_amr_row',
new Date('2026-07-02T08:15:00.000Z'),
);
bindSignedInUserAttributionPersonProperties(
'usr_amr_42',
new Date('2026-07-02T08:30:00.000Z'),
);
expect(setAnalyticsPersonProperties).toHaveBeenLastCalledWith(
expect.objectContaining({
od_app_user_id: 'usr_amr_42',
od_source_resolved: 'social',
od_source_resolution: 'onboarding',
od_onboarding_at: '2026-07-01T07:00:00.000Z',
od_amr_entry_id: expect.stringMatching(/^od-amr-/u),
od_amr_entry_source: 'inline_model_switcher_amr_row',
od_amr_entry_at: '2026-07-02T08:15:00.000Z',
}),
);
});
it('does not emit an empty bind when the signed-in user id is missing', () => {
bindSignedInUserAttributionPersonProperties(null);
expect(setAnalyticsPersonProperties).not.toHaveBeenCalled();
});
});
function writeLandingAttributionCookie(value: Record<string, unknown>): void {
document.cookie = `od_attr=${encodeURIComponent(JSON.stringify(value))};path=/`;
}