improvements to identify

This commit is contained in:
bkellam 2025-12-11 12:15:49 -08:00
parent 36338dc8d2
commit eac7ea0555
5 changed files with 68 additions and 21 deletions

View file

@ -7,6 +7,7 @@ import { Toaster } from "@/components/ui/toaster";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
import { SessionProvider } from "next-auth/react"; import { SessionProvider } from "next-auth/react";
import { env } from "@sourcebot/shared"; import { env } from "@sourcebot/shared";
import { env as clientEnv } from "@sourcebot/shared/client";
import { PlanProvider } from "@/features/entitlements/planProvider"; import { PlanProvider } from "@/features/entitlements/planProvider";
import { getEntitlements } from "@sourcebot/shared"; 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, // @note: the posthog api key doesn't need to be kept secret,
// so we are safe to send it to the client. // so we are safe to send it to the client.
posthogApiKey={env.POSTHOG_PAPIK} posthogApiKey={env.POSTHOG_PAPIK}
sourcebotVersion={clientEnv.NEXT_PUBLIC_SOURCEBOT_VERSION}
sourcebotInstallId={env.SOURCEBOT_INSTALL_ID}
> >
<ThemeProvider <ThemeProvider
attribute="class" attribute="class"

View file

@ -34,9 +34,17 @@ interface PostHogProviderProps {
children: React.ReactNode children: React.ReactNode
isDisabled: boolean isDisabled: boolean
posthogApiKey: string posthogApiKey: string
sourcebotVersion: string
sourcebotInstallId: string
} }
export function PostHogProvider({ children, isDisabled, posthogApiKey }: PostHogProviderProps) { export function PostHogProvider({
children,
isDisabled,
posthogApiKey,
sourcebotVersion,
sourcebotInstallId,
}: PostHogProviderProps) {
const { data: session } = useSession(); const { data: session } = useSession();
useEffect(() => { useEffect(() => {
@ -61,27 +69,33 @@ export function PostHogProvider({ children, isDisabled, posthogApiKey }: PostHog
'$referrer', '$referrer',
'$referring_domain', '$referring_domain',
'$ip', '$ip',
] : [] ] : [],
loaded: (posthog) => {
// Include install id & version in all events.
posthog.register({
sourcebot_version: sourcebotVersion,
install_id: sourcebotInstallId,
});
}
}); });
} else { } else {
console.debug("PostHog telemetry disabled"); console.debug("PostHog telemetry disabled");
} }
}, [isDisabled, posthogApiKey]); }, [isDisabled, posthogApiKey, sourcebotInstallId, sourcebotVersion]);
useEffect(() => { useEffect(() => {
if (!session) { if (!session) {
return; return;
} }
// Only identify the user if we are running in a cloud environment. posthog.identify(
if (env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT !== undefined) { 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, email: session.user.email,
name: session.user.name, name: session.user.name,
}); } : undefined
} else { );
console.debug("PostHog identify skipped");
}
}, [session]); }, [session]);
return ( return (

View file

@ -3,17 +3,13 @@
import { CaptureOptions } from "posthog-js"; import { CaptureOptions } from "posthog-js";
import posthog from "posthog-js"; import posthog from "posthog-js";
import { PosthogEvent, PosthogEventMap } from "../lib/posthogEvents"; import { PosthogEvent, PosthogEventMap } from "../lib/posthogEvents";
import { env } from "@sourcebot/shared/client";
export function captureEvent<E extends PosthogEvent>(event: E, properties: PosthogEventMap[E], options?: CaptureOptions) { export function captureEvent<E extends PosthogEvent>(event: E, properties: PosthogEventMap[E], options?: CaptureOptions) {
if(!options) { if(!options) {
options = {}; options = {};
} }
options.send_instantly = true; options.send_instantly = true;
posthog.capture(event, { posthog.capture(event, properties, options);
...properties,
sourcebot_version: env.NEXT_PUBLIC_SOURCEBOT_VERSION,
}, options);
} }
/** /**

View file

@ -1,9 +1,12 @@
import { PostHog } from 'posthog-node' import { PostHog } from 'posthog-node'
import { env } from '@sourcebot/shared' import { env } from '@sourcebot/shared'
import { env as clientEnv } from '@sourcebot/shared/client';
import { RequestCookies } from 'next/dist/compiled/@edge-runtime/cookies'; import { RequestCookies } from 'next/dist/compiled/@edge-runtime/cookies';
import * as Sentry from "@sentry/nextjs"; import * as Sentry from "@sentry/nextjs";
import { PosthogEvent, PosthogEventMap } from './posthogEvents'; 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 * @note: This is a subset of the properties stored in the
@ -47,13 +50,40 @@ const getPostHogCookie = (cookieStore: Pick<RequestCookies, 'get'>): PostHogCook
return undefined; 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<E extends PosthogEvent>(event: E, properties: PosthogEventMap[E]) { export async function captureEvent<E extends PosthogEvent>(event: E, properties: PosthogEventMap[E]) {
if (env.SOURCEBOT_TELEMETRY_DISABLED === 'true') { if (env.SOURCEBOT_TELEMETRY_DISABLED === 'true') {
return; return;
} }
const cookieStore = await cookies(); const distinctId = await tryGetDistinctId();
const cookie = getPostHogCookie(cookieStore);
const posthog = new PostHog(env.POSTHOG_PAPIK, { const posthog = new PostHog(env.POSTHOG_PAPIK, {
host: 'https://us.i.posthog.com', host: 'https://us.i.posthog.com',
@ -63,7 +93,11 @@ export async function captureEvent<E extends PosthogEvent>(event: E, properties:
posthog.capture({ posthog.capture({
event, event,
properties, properties: {
distinctId: cookie?.distinct_id ?? '', ...properties,
sourcebot_version: clientEnv.NEXT_PUBLIC_SOURCEBOT_VERSION,
install_id: env.SOURCEBOT_INSTALL_ID,
},
distinctId,
}); });
} }

View file

@ -156,7 +156,7 @@ export const getAuthenticatedUser = async () => {
/** /**
* Returns a API key object if the API key string is valid, otherwise returns undefined. * Returns a API key object if the API key string is valid, otherwise returns undefined.
*/ */
const getVerifiedApiObject = async (apiKeyString: string): Promise<ApiKey | undefined> => { export const getVerifiedApiObject = async (apiKeyString: string): Promise<ApiKey | undefined> => {
const parts = apiKeyString.split("-"); const parts = apiKeyString.split("-");
if (parts.length !== 2 || parts[0] !== "sourcebot") { if (parts.length !== 2 || parts[0] !== "sourcebot") {
return undefined; return undefined;