Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions apps/code/src/renderer/utils/analytics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

const mockPosthog = {
init: vi.fn(),
register: vi.fn(),
onFeatureFlags: vi.fn(),
isFeatureEnabled: vi.fn(),
startSessionRecording: vi.fn(),
capture: vi.fn(),
identify: vi.fn(),
group: vi.fn(),
reset: vi.fn(),
captureException: vi.fn(),
reloadFeatureFlags: vi.fn(),
};

vi.mock("posthog-js/dist/module.full.no-external", () => ({
default: mockPosthog,
}));

vi.mock("posthog-js/dist/posthog-recorder", () => ({}));

async function loadAnalytics() {
vi.resetModules();
return await import("./analytics");
}

beforeEach(() => {
vi.clearAllMocks();
vi.stubEnv("VITE_POSTHOG_API_KEY", "test-key");
});

afterEach(() => {
vi.unstubAllEnvs();
});

describe("onFeatureFlagsLoaded", () => {
it("delivers pre-init subscribers when init runs", async () => {
const { initializePostHog, onFeatureFlagsLoaded } = await loadAnalytics();

const cb = vi.fn();
onFeatureFlagsLoaded(cb);

expect(mockPosthog.onFeatureFlags).not.toHaveBeenCalled();

initializePostHog();

expect(mockPosthog.onFeatureFlags).toHaveBeenCalledTimes(1);
expect(mockPosthog.onFeatureFlags).toHaveBeenCalledWith(cb);
});

it("does not register a buffered listener that unsubscribed before init", async () => {
const { initializePostHog, onFeatureFlagsLoaded } = await loadAnalytics();

const cb = vi.fn();
const off = onFeatureFlagsLoaded(cb);
off();

initializePostHog();

expect(mockPosthog.onFeatureFlags).not.toHaveBeenCalled();
});

it("propagates unsubscribe to PostHog when called after init", async () => {
const realUnsub = vi.fn();
mockPosthog.onFeatureFlags.mockReturnValue(realUnsub);

const { initializePostHog, onFeatureFlagsLoaded } = await loadAnalytics();

const off = onFeatureFlagsLoaded(vi.fn());
initializePostHog();
off();

expect(realUnsub).toHaveBeenCalledTimes(1);
});

it("routes post-init subscribers directly to PostHog", async () => {
const realUnsub = vi.fn();
mockPosthog.onFeatureFlags.mockReturnValue(realUnsub);

const { initializePostHog, onFeatureFlagsLoaded } = await loadAnalytics();
initializePostHog();

const cb = vi.fn();
const off = onFeatureFlagsLoaded(cb);

expect(mockPosthog.onFeatureFlags).toHaveBeenCalledWith(cb);

off();
expect(realUnsub).toHaveBeenCalledTimes(1);
});
});

describe("initializePostHog", () => {
it("is idempotent across repeat calls", async () => {
const { initializePostHog } = await loadAnalytics();

initializePostHog();
initializePostHog();

expect(mockPosthog.init).toHaveBeenCalledTimes(1);
});

it("does nothing when no API key is set", async () => {
vi.stubEnv("VITE_POSTHOG_API_KEY", "");
const { initializePostHog, onFeatureFlagsLoaded } = await loadAnalytics();

const cb = vi.fn();
onFeatureFlagsLoaded(cb);
initializePostHog();

expect(mockPosthog.init).not.toHaveBeenCalled();
expect(mockPosthog.onFeatureFlags).not.toHaveBeenCalled();
});
});
27 changes: 24 additions & 3 deletions apps/code/src/renderer/utils/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ const log = logger.scope("analytics");

let isInitialized = false;

type PendingFlagListener = {
callback: () => void;
unsubscribe: (() => void) | null;
};

// Subscribers added before initializePostHog runs.
const pendingFlagListeners = new Set<PendingFlagListener>();

export function initializePostHog() {
const apiKey = import.meta.env.VITE_POSTHOG_API_KEY;
const apiHost =
Expand Down Expand Up @@ -44,6 +52,11 @@ export function initializePostHog() {
posthog.register({ team: "posthog-code" });

isInitialized = true;

for (const listener of pendingFlagListeners) {
listener.unsubscribe = posthog.onFeatureFlags(listener.callback);
}
pendingFlagListeners.clear();
}
Comment thread
charlesvien marked this conversation as resolved.

/**
Expand Down Expand Up @@ -212,11 +225,19 @@ export function isFeatureFlagEnabled(flagKey: string): boolean {
* Returns unsubscribe function.
*/
export function onFeatureFlagsLoaded(callback: () => void): () => void {
if (!isInitialized) {
return () => {};
if (isInitialized) {
return posthog.onFeatureFlags(callback);
}

return posthog.onFeatureFlags(callback);
const listener: PendingFlagListener = { callback, unsubscribe: null };
pendingFlagListeners.add(listener);
return () => {
if (listener.unsubscribe) {
listener.unsubscribe();
} else {
pendingFlagListeners.delete(listener);
}
};
}

/**
Expand Down
Loading