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
100 changes: 100 additions & 0 deletions apps/code/src/main/services/posthog-analytics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

const mockCapture = vi.hoisted(() => vi.fn());
const mockCaptureException = vi.hoisted(() => vi.fn());
const mockIdentify = vi.hoisted(() => vi.fn());
const mockShutdown = vi.hoisted(() => vi.fn());
const MockPostHog = vi.hoisted(() => vi.fn());

vi.mock("posthog-node", () => ({ PostHog: MockPostHog }));

import {
captureException,
initializePostHog,
resetUser,
shutdownPostHog,
trackAppEvent,
} from "./posthog-analytics";

describe("posthog-analytics", () => {
beforeEach(() => {
vi.clearAllMocks();
MockPostHog.mockImplementation(function (this: Record<string, unknown>) {
this.capture = mockCapture;
this.captureException = mockCaptureException;
this.identify = mockIdentify;
this.shutdown = mockShutdown;
});
process.env.VITE_POSTHOG_API_KEY = "test-key";
resetUser();
initializePostHog();
});

afterEach(async () => {
await shutdownPostHog();
});

it("includes the app version on every tracked event", () => {
trackAppEvent("app_started");

expect(mockCapture).toHaveBeenCalledWith(
expect.objectContaining({
event: "app_started",
properties: expect.objectContaining({
team: "posthog-code",
app_version: "0.0.0-test",
}),
}),
);
});

it("lets caller-supplied properties coexist with the app version", () => {
trackAppEvent("app_quit", { reason: "user-initiated" });

expect(mockCapture).toHaveBeenCalledWith(
expect.objectContaining({
properties: expect.objectContaining({
reason: "user-initiated",
app_version: "0.0.0-test",
}),
}),
);
});

it("does not let caller-supplied app_version override the system value", () => {
trackAppEvent("app_quit", { app_version: "spoofed" });

expect(mockCapture).toHaveBeenCalledWith(
expect.objectContaining({
properties: expect.objectContaining({
app_version: "0.0.0-test",
}),
}),
);
});

it("includes the app version on captured exceptions", () => {
captureException(new Error("boom"));

expect(mockCaptureException).toHaveBeenCalledWith(
expect.any(Error),
expect.any(String),
expect.objectContaining({
team: "posthog-code",
app_version: "0.0.0-test",
}),
);
});

it("does not let additionalProperties override app_version on exceptions", () => {
captureException(new Error("boom"), { app_version: "spoofed" });

expect(mockCaptureException).toHaveBeenCalledWith(
expect.any(Error),
expect.any(String),
expect.objectContaining({
app_version: "0.0.0-test",
}),
);
});
});
3 changes: 3 additions & 0 deletions apps/code/src/main/services/posthog-analytics.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { PostHog } from "posthog-node";
import { getAppVersion } from "../utils/env";

let posthogClient: PostHog | null = null;
let currentUserId: string | null = null;
Expand Down Expand Up @@ -47,6 +48,7 @@ export function trackAppEvent(
properties: {
team: "posthog-code",
...properties,
app_version: getAppVersion(),
$process_person_profile: !!currentUserId,
},
Comment thread
andrewm4894 marked this conversation as resolved.
});
Expand Down Expand Up @@ -95,5 +97,6 @@ export function captureException(
posthogClient.captureException(error, distinctId, {
team: "posthog-code",
...additionalProperties,
app_version: getAppVersion(),
});
Comment thread
andrewm4894 marked this conversation as resolved.
}
10 changes: 8 additions & 2 deletions apps/code/src/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { isNotAuthenticatedError } from "@shared/errors";
import { ANALYTICS_EVENTS } from "@shared/types/analytics";
import { useQueryClient } from "@tanstack/react-query";
import { useSubscription } from "@trpc/tanstack-react-query";
import { initializePostHog, track } from "@utils/analytics";
import { initializePostHog, registerAppVersion, track } from "@utils/analytics";
import { logger } from "@utils/logger";
import { toast } from "@utils/toast";
import { AnimatePresence, motion } from "framer-motion";
Expand All @@ -48,9 +48,15 @@ function App() {
const [showTransition, setShowTransition] = useState(false);
const wasInMainApp = useRef(isAuthenticated && hasCompletedOnboarding);

// Initialize PostHog analytics
// Initialize PostHog analytics and register the app version super property.
useEffect(() => {
initializePostHog();
trpcClient.os.getAppVersion
.query()
.then(registerAppVersion)
Comment thread
andrewm4894 marked this conversation as resolved.
Comment thread
andrewm4894 marked this conversation as resolved.
.catch((error) => {
log.warn("Failed to register app version super property", { error });
});
}, []);

// Initialize connectivity monitoring
Expand Down
35 changes: 35 additions & 0 deletions apps/code/src/renderer/utils/analytics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,41 @@ describe("onFeatureFlagsLoaded", () => {
});
});

describe("registerAppVersion", () => {
it("registers app_version as a super property after init", async () => {
const { initializePostHog, registerAppVersion } = await loadAnalytics();

initializePostHog();
registerAppVersion("1.2.3");

expect(mockPosthog.register).toHaveBeenCalledWith({ app_version: "1.2.3" });
});

it("does nothing before init", async () => {
const { registerAppVersion } = await loadAnalytics();

registerAppVersion("1.2.3");

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

it("re-registers app_version after resetUser clears super properties", async () => {
const { initializePostHog, registerAppVersion, resetUser } =
await loadAnalytics();

initializePostHog();
registerAppVersion("1.2.3");

resetUser();

expect(mockPosthog.reset).toHaveBeenCalledTimes(1);
expect(mockPosthog.register).toHaveBeenLastCalledWith({
team: "posthog-code",
app_version: "1.2.3",
});
});
});

describe("initializePostHog", () => {
it("is idempotent across repeat calls", async () => {
const { initializePostHog } = await loadAnalytics();
Expand Down
31 changes: 29 additions & 2 deletions apps/code/src/renderer/utils/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@ const log = logger.scope("analytics");

let isInitialized = false;

// Cached so it can be re-applied after posthog.reset() clears super properties.
let registeredAppVersion: string | null = null;

// posthog.reset() wipes super properties, so these are re-registered after each reset.
function registerPersistentSuperProperties() {
posthog.register({
team: "posthog-code",
...(registeredAppVersion !== null
? { app_version: registeredAppVersion }
: {}),
});
}

type PendingFlagListener = {
callback: () => void;
unsubscribe: (() => void) | null;
Expand Down Expand Up @@ -49,10 +62,10 @@ export function initializePostHog() {
},
});

posthog.register({ team: "posthog-code" });

isInitialized = true;

registerPersistentSuperProperties();

for (const listener of pendingFlagListeners) {
listener.unsubscribe = posthog.onFeatureFlags(listener.callback);
}
Expand Down Expand Up @@ -102,6 +115,17 @@ export function startSessionRecording() {
}, 1000);
}

// Register the app version as a super property so it rides along on every event.
export function registerAppVersion(appVersion: string) {
registeredAppVersion = appVersion;

if (!isInitialized) {
return;
}

posthog.register({ app_version: appVersion });
}

export function identifyUser(
userId: string,
properties?: UserIdentifyProperties,
Expand Down Expand Up @@ -146,6 +170,9 @@ export function resetUser() {
}

posthog.reset();

// reset() clears super properties; re-apply the persistent ones.
registerPersistentSuperProperties();
}

export function track<K extends keyof EventPropertyMap>(
Expand Down
2 changes: 2 additions & 0 deletions apps/code/vite-plugin-auto-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export function autoServicesPlugin(servicesDir: string): Plugin {
(f) =>
f.endsWith(".ts") &&
!f.endsWith(".types.ts") &&
!f.endsWith(".test.ts") &&
!f.endsWith(".spec.ts") &&
f !== "index.ts" &&
f !== "types.ts",
);
Expand Down
Loading