diff --git a/packages/web/src/app/layout.tsx b/packages/web/src/app/layout.tsx index 13adfe05..1777b357 100644 --- a/packages/web/src/app/layout.tsx +++ b/packages/web/src/app/layout.tsx @@ -7,6 +7,7 @@ import { Toaster } from "@/components/ui/toaster"; import { TooltipProvider } from "@/components/ui/tooltip"; import { SessionProvider } from "next-auth/react"; import { env } from "@sourcebot/shared"; +import { env as clientEnv } from "@sourcebot/shared/client"; import { PlanProvider } from "@/features/entitlements/planProvider"; import { getEntitlements } from "@sourcebot/shared"; @@ -42,6 +43,8 @@ export default function RootLayout({ // @note: the posthog api key doesn't need to be kept secret, // so we are safe to send it to the client. posthogApiKey={env.POSTHOG_PAPIK} + sourcebotVersion={clientEnv.NEXT_PUBLIC_SOURCEBOT_VERSION} + sourcebotInstallId={env.SOURCEBOT_INSTALL_ID} > { @@ -61,27 +69,33 @@ export function PostHogProvider({ children, isDisabled, posthogApiKey }: PostHog '$referrer', '$referring_domain', '$ip', - ] : [] + ] : [], + loaded: (posthog) => { + // Include install id & version in all events. + posthog.register({ + sourcebot_version: sourcebotVersion, + install_id: sourcebotInstallId, + }); + } }); } else { console.debug("PostHog telemetry disabled"); } - }, [isDisabled, posthogApiKey]); + }, [isDisabled, posthogApiKey, sourcebotInstallId, sourcebotVersion]); useEffect(() => { if (!session) { return; } - // Only identify the user if we are running in a cloud environment. - if (env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT !== undefined) { - posthog.identify(session.user.id, { + posthog.identify( + session.user.id, + // Only include email & name when running in a cloud environment. + env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT !== undefined ? { email: session.user.email, name: session.user.name, - }); - } else { - console.debug("PostHog identify skipped"); - } + } : undefined + ); }, [session]); return ( diff --git a/packages/web/src/hooks/useCaptureEvent.ts b/packages/web/src/hooks/useCaptureEvent.ts index 597f73ca..70652afb 100644 --- a/packages/web/src/hooks/useCaptureEvent.ts +++ b/packages/web/src/hooks/useCaptureEvent.ts @@ -3,17 +3,13 @@ import { CaptureOptions } from "posthog-js"; import posthog from "posthog-js"; import { PosthogEvent, PosthogEventMap } from "../lib/posthogEvents"; -import { env } from "@sourcebot/shared/client"; export function captureEvent(event: E, properties: PosthogEventMap[E], options?: CaptureOptions) { if(!options) { options = {}; } options.send_instantly = true; - posthog.capture(event, { - ...properties, - sourcebot_version: env.NEXT_PUBLIC_SOURCEBOT_VERSION, - }, options); + posthog.capture(event, properties, options); } /** diff --git a/packages/web/src/lib/posthog.ts b/packages/web/src/lib/posthog.ts index 6296768e..5cc8c484 100644 --- a/packages/web/src/lib/posthog.ts +++ b/packages/web/src/lib/posthog.ts @@ -1,9 +1,12 @@ import { PostHog } from 'posthog-node' import { env } from '@sourcebot/shared' +import { env as clientEnv } from '@sourcebot/shared/client'; import { RequestCookies } from 'next/dist/compiled/@edge-runtime/cookies'; import * as Sentry from "@sentry/nextjs"; import { PosthogEvent, PosthogEventMap } from './posthogEvents'; -import { cookies } from 'next/headers'; +import { cookies, headers } from 'next/headers'; +import { auth } from '@/auth'; +import { getVerifiedApiObject } from '@/withAuthV2'; /** * @note: This is a subset of the properties stored in the @@ -47,13 +50,40 @@ const getPostHogCookie = (cookieStore: Pick): PostHogCook return undefined; } +/** + * Attempts to retrieve the distinct id of the current user. + */ +const tryGetDistinctId = async () => { + // First, attempt to retrieve the distinct id from the cookie. + const cookieStore = await cookies(); + const cookie = getPostHogCookie(cookieStore); + if (cookie) { + return cookie.distinct_id; + } + + // Next, from the session. + const session = await auth(); + if (session) { + return session.user.id; + } + + // Finally, from the api key. + const headersList = await headers(); + const apiKeyString = headersList.get("X-Sourcebot-Api-Key") ?? undefined; + if (!apiKeyString) { + return undefined; + } + + const apiKey = await getVerifiedApiObject(apiKeyString); + return apiKey?.createdById; +} + export async function captureEvent(event: E, properties: PosthogEventMap[E]) { if (env.SOURCEBOT_TELEMETRY_DISABLED === 'true') { return; } - const cookieStore = await cookies(); - const cookie = getPostHogCookie(cookieStore); + const distinctId = await tryGetDistinctId(); const posthog = new PostHog(env.POSTHOG_PAPIK, { host: 'https://us.i.posthog.com', @@ -63,7 +93,11 @@ export async function captureEvent(event: E, properties: posthog.capture({ event, - properties, - distinctId: cookie?.distinct_id ?? '', + properties: { + ...properties, + sourcebot_version: clientEnv.NEXT_PUBLIC_SOURCEBOT_VERSION, + install_id: env.SOURCEBOT_INSTALL_ID, + }, + distinctId, }); } \ No newline at end of file diff --git a/packages/web/src/withAuthV2.ts b/packages/web/src/withAuthV2.ts index f1e22962..88cb763b 100644 --- a/packages/web/src/withAuthV2.ts +++ b/packages/web/src/withAuthV2.ts @@ -156,7 +156,7 @@ export const getAuthenticatedUser = async () => { /** * Returns a API key object if the API key string is valid, otherwise returns undefined. */ -const getVerifiedApiObject = async (apiKeyString: string): Promise => { +export const getVerifiedApiObject = async (apiKeyString: string): Promise => { const parts = apiKeyString.split("-"); if (parts.length !== 2 || parts[0] !== "sourcebot") { return undefined;