dev #81
37
apps/web/src/lib/__tests__/posthog.test.ts
Normal file
37
apps/web/src/lib/__tests__/posthog.test.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
// Mock the underlying posthog-js SDK (not @/lib/posthog) so we exercise the
|
||||
// real capturePageView implementation.
|
||||
const captureMock = vi.fn();
|
||||
vi.mock("posthog-js", () => ({
|
||||
default: {
|
||||
init: vi.fn(),
|
||||
capture: captureMock,
|
||||
identify: vi.fn(),
|
||||
reset: vi.fn(),
|
||||
people: { set: vi.fn() },
|
||||
},
|
||||
}));
|
||||
|
||||
import { capturePageView } from "../posthog";
|
||||
|
||||
describe("capturePageView", () => {
|
||||
beforeEach(() => {
|
||||
captureMock.mockClear();
|
||||
});
|
||||
|
||||
it("includes the query string so UTM params are not dropped (attribution regression)", async () => {
|
||||
window.history.pushState({}, "", "/?utm_source=facebook&utm_campaign=traffic_lpv_v1");
|
||||
|
||||
capturePageView("/");
|
||||
|
||||
await vi.waitFor(() => expect(captureMock).toHaveBeenCalledTimes(1));
|
||||
const [event, props] = captureMock.mock.calls[0];
|
||||
expect(event).toBe("$pageview");
|
||||
// The whole point: $current_url must carry the UTMs, not just origin+path.
|
||||
expect(props.$current_url).toContain("utm_source=facebook");
|
||||
expect(props.$current_url).toContain("utm_campaign=traffic_lpv_v1");
|
||||
expect(props.$current_url).toBe(window.location.href);
|
||||
expect(props.$pathname).toBe("/");
|
||||
});
|
||||
});
|
||||
@@ -25,7 +25,11 @@ export function initPostHog(): void {
|
||||
ph.init(key, {
|
||||
api_host: "https://t.sase.tr",
|
||||
defaults: "2026-01-30",
|
||||
person_profiles: "identified_only",
|
||||
// "always" (not "identified_only") so anonymous ad visitors get a person
|
||||
// profile that captures first-touch UTM/referrer ($initial_utm_*). Required
|
||||
// for ad→signup attribution. Volume is ~34k events/mo (PostHog free tier is
|
||||
// 1M/mo), so the cost impact is negligible at current scale.
|
||||
person_profiles: "always",
|
||||
capture_pageview: false,
|
||||
capture_pageleave: false,
|
||||
autocapture: false,
|
||||
@@ -61,7 +65,14 @@ export function capture(event: string, properties?: Record<string, unknown>): vo
|
||||
}
|
||||
|
||||
export function capturePageView(path: string): void {
|
||||
load().then((ph) => ph.capture("$pageview", { $current_url: window.location.origin + path }));
|
||||
// Pass the full URL (incl. query string) so PostHog can parse UTM params and
|
||||
// persist first-touch attribution ($initial_utm_*, $utm_*). Previously this
|
||||
// sent only `origin + path`, silently dropping every ad UTM — so all paid
|
||||
// traffic was mis-bucketed as "direct". The index route keeps unknown search
|
||||
// params, so window.location.href still holds the UTMs at mount.
|
||||
load().then((ph) =>
|
||||
ph.capture("$pageview", { $current_url: window.location.href, $pathname: path }),
|
||||
);
|
||||
}
|
||||
|
||||
export function setPeopleProperties(properties: Record<string, unknown>): void {
|
||||
|
||||
Reference in New Issue
Block a user