From c41087acdf5025c179d109cd2ccd9658a1f5fbe4 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Fri, 12 Dec 2025 12:40:47 -0800 Subject: [PATCH] chore(web): PostHog telemetry improvements (#672) --- .../navigationMenu/trialIndicator.tsx | 2 +- .../components/changeOrgDomainCard.tsx | 2 +- .../components/changeOrgNameCard.tsx | 2 +- .../members/components/inviteMemberCard.tsx | 2 +- .../members/components/invitesList.tsx | 2 +- .../members/components/membersList.tsx | 6 +- .../members/components/requestsList.tsx | 4 +- packages/web/src/app/layout.tsx | 3 + packages/web/src/app/posthogProvider.tsx | 34 ++-- .../components/changeBillingEmailCard.tsx | 2 +- .../features/billing/components/checkout.tsx | 4 +- .../components/manageSubscriptionButton.tsx | 2 +- .../billing/components/teamUpgradeCard.tsx | 2 +- packages/web/src/hooks/useCaptureEvent.ts | 6 +- packages/web/src/lib/posthog.ts | 48 +++++- packages/web/src/lib/posthogEvents.ts | 162 ++---------------- packages/web/src/withAuthV2.ts | 2 +- 17 files changed, 101 insertions(+), 184 deletions(-) diff --git a/packages/web/src/app/[domain]/components/navigationMenu/trialIndicator.tsx b/packages/web/src/app/[domain]/components/navigationMenu/trialIndicator.tsx index f7af06fc..15adc948 100644 --- a/packages/web/src/app/[domain]/components/navigationMenu/trialIndicator.tsx +++ b/packages/web/src/app/[domain]/components/navigationMenu/trialIndicator.tsx @@ -19,7 +19,7 @@ export const TrialIndicator = ({ subscription }: Props) => { if (isServiceError(subscription)) { captureEvent('wa_trial_nav_subscription_fetch_fail', { - error: subscription.errorCode, + errorCode: subscription.errorCode, }); return null; } diff --git a/packages/web/src/app/[domain]/settings/(general)/components/changeOrgDomainCard.tsx b/packages/web/src/app/[domain]/settings/(general)/components/changeOrgDomainCard.tsx index 29bea133..484b8427 100644 --- a/packages/web/src/app/[domain]/settings/(general)/components/changeOrgDomainCard.tsx +++ b/packages/web/src/app/[domain]/settings/(general)/components/changeOrgDomainCard.tsx @@ -50,7 +50,7 @@ export function ChangeOrgDomainCard({ orgDomain, currentUserRole, rootDomain }: description: `❌ Failed to update organization url. Reason: ${result.message}`, }) captureEvent('wa_org_domain_updated_fail', { - error: result.errorCode, + errorCode: result.errorCode, }); } else { toast({ diff --git a/packages/web/src/app/[domain]/settings/(general)/components/changeOrgNameCard.tsx b/packages/web/src/app/[domain]/settings/(general)/components/changeOrgNameCard.tsx index d6c99fc7..b83eae80 100644 --- a/packages/web/src/app/[domain]/settings/(general)/components/changeOrgNameCard.tsx +++ b/packages/web/src/app/[domain]/settings/(general)/components/changeOrgNameCard.tsx @@ -48,7 +48,7 @@ export function ChangeOrgNameCard({ orgName, currentUserRole }: ChangeOrgNameCar description: `❌ Failed to update organization name. Reason: ${result.message}`, }) captureEvent('wa_org_name_updated_fail', { - error: result.errorCode, + errorCode: result.errorCode, }); } else { toast({ diff --git a/packages/web/src/app/[domain]/settings/members/components/inviteMemberCard.tsx b/packages/web/src/app/[domain]/settings/members/components/inviteMemberCard.tsx index 2e0aa15e..e0808eac 100644 --- a/packages/web/src/app/[domain]/settings/members/components/inviteMemberCard.tsx +++ b/packages/web/src/app/[domain]/settings/members/components/inviteMemberCard.tsx @@ -62,7 +62,7 @@ export const InviteMemberCard = ({ currentUserRole, isBillingEnabled, seatsAvail description: `❌ Failed to invite members. Reason: ${res.message}` }); captureEvent('wa_invite_member_card_invite_fail', { - error: res.errorCode, + errorCode: res.errorCode, num_emails: data.emails.length, }); } else { diff --git a/packages/web/src/app/[domain]/settings/members/components/invitesList.tsx b/packages/web/src/app/[domain]/settings/members/components/invitesList.tsx index 81f56862..66d09665 100644 --- a/packages/web/src/app/[domain]/settings/members/components/invitesList.tsx +++ b/packages/web/src/app/[domain]/settings/members/components/invitesList.tsx @@ -60,7 +60,7 @@ export const InvitesList = ({ invites, currentUserRole }: InviteListProps) => { description: `❌ Failed to cancel invite. Reason: ${response.message}` }) captureEvent('wa_invites_list_cancel_invite_fail', { - error: response.errorCode, + errorCode: response.errorCode, }) } else { toast({ diff --git a/packages/web/src/app/[domain]/settings/members/components/membersList.tsx b/packages/web/src/app/[domain]/settings/members/components/membersList.tsx index e19860e5..35b1da48 100644 --- a/packages/web/src/app/[domain]/settings/members/components/membersList.tsx +++ b/packages/web/src/app/[domain]/settings/members/components/membersList.tsx @@ -71,7 +71,7 @@ export const MembersList = ({ members, currentUserId, currentUserRole, orgName } description: `❌ Failed to remove member. Reason: ${response.message}` }) captureEvent('wa_members_list_remove_member_fail', { - error: response.errorCode, + errorCode: response.errorCode, }) } else { toast({ @@ -91,7 +91,7 @@ export const MembersList = ({ members, currentUserId, currentUserRole, orgName } description: `❌ Failed to transfer ownership. Reason: ${response.message}` }) captureEvent('wa_members_list_transfer_ownership_fail', { - error: response.errorCode, + errorCode: response.errorCode, }) } else { toast({ @@ -111,7 +111,7 @@ export const MembersList = ({ members, currentUserId, currentUserRole, orgName } description: `❌ Failed to leave organization. Reason: ${response.message}` }) captureEvent('wa_members_list_leave_org_fail', { - error: response.errorCode, + errorCode: response.errorCode, }) } else { toast({ diff --git a/packages/web/src/app/[domain]/settings/members/components/requestsList.tsx b/packages/web/src/app/[domain]/settings/members/components/requestsList.tsx index c78e8eea..f3ae55f3 100644 --- a/packages/web/src/app/[domain]/settings/members/components/requestsList.tsx +++ b/packages/web/src/app/[domain]/settings/members/components/requestsList.tsx @@ -63,7 +63,7 @@ export const RequestsList = ({ requests, currentUserRole }: RequestsListProps) = description: `❌ Failed to approve request. Reason: ${response.message}` }) captureEvent('wa_requests_list_approve_request_fail', { - error: response.errorCode, + errorCode: response.errorCode, }) } else { toast({ @@ -83,7 +83,7 @@ export const RequestsList = ({ requests, currentUserRole }: RequestsListProps) = description: `❌ Failed to reject request.` }) captureEvent('wa_requests_list_reject_request_fail', { - error: response.errorCode, + errorCode: response.errorCode, }) } else { toast({ 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/ee/features/billing/components/changeBillingEmailCard.tsx b/packages/web/src/ee/features/billing/components/changeBillingEmailCard.tsx index a4febbe5..674b8fac 100644 --- a/packages/web/src/ee/features/billing/components/changeBillingEmailCard.tsx +++ b/packages/web/src/ee/features/billing/components/changeBillingEmailCard.tsx @@ -55,7 +55,7 @@ export function ChangeBillingEmailCard({ currentUserRole, billingEmail }: Change description: "❌ Failed to update billing email. Please try again.", }) captureEvent('wa_billing_email_updated_fail', { - error: result.message, + errorCode: result.errorCode, }) } setIsLoading(false) diff --git a/packages/web/src/ee/features/billing/components/checkout.tsx b/packages/web/src/ee/features/billing/components/checkout.tsx index 980c6cf2..41dc7dc4 100644 --- a/packages/web/src/ee/features/billing/components/checkout.tsx +++ b/packages/web/src/ee/features/billing/components/checkout.tsx @@ -30,7 +30,7 @@ export const Checkout = () => { variant: "destructive", }); captureEvent('wa_onboard_checkout_fail', { - error: errorMessage, + errorCode: errorMessage, }); } }, [errorCode, errorMessage, toast, captureEvent]); @@ -45,7 +45,7 @@ export const Checkout = () => { variant: "destructive", }) captureEvent('wa_onboard_checkout_fail', { - error: response.errorCode, + errorCode: response.errorCode, }); } else { captureEvent('wa_onboard_checkout_success', {}); diff --git a/packages/web/src/ee/features/billing/components/manageSubscriptionButton.tsx b/packages/web/src/ee/features/billing/components/manageSubscriptionButton.tsx index a2c2701b..9dfa158f 100644 --- a/packages/web/src/ee/features/billing/components/manageSubscriptionButton.tsx +++ b/packages/web/src/ee/features/billing/components/manageSubscriptionButton.tsx @@ -21,7 +21,7 @@ export function ManageSubscriptionButton({ currentUserRole }: { currentUserRole: const session = await getCustomerPortalSessionLink(domain); if (isServiceError(session)) { captureEvent('wa_manage_subscription_button_create_portal_session_fail', { - error: session.errorCode, + errorCode: session.errorCode, }); setIsLoading(false); } else { diff --git a/packages/web/src/ee/features/billing/components/teamUpgradeCard.tsx b/packages/web/src/ee/features/billing/components/teamUpgradeCard.tsx index db155176..26ff5276 100644 --- a/packages/web/src/ee/features/billing/components/teamUpgradeCard.tsx +++ b/packages/web/src/ee/features/billing/components/teamUpgradeCard.tsx @@ -32,7 +32,7 @@ export const TeamUpgradeCard = ({ buttonText }: TeamUpgradeCardProps) => { variant: "destructive", }); captureEvent('wa_team_upgrade_checkout_fail', { - error: response.errorCode, + errorCode: response.errorCode, }); } else { router.push(response.url); 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..9adfba1b 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,43 @@ 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 headersList = await headers(); + const host = headersList.get("host") ?? undefined; const posthog = new PostHog(env.POSTHOG_PAPIK, { host: 'https://us.i.posthog.com', @@ -63,7 +96,12 @@ 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, + $host: host, + }, + distinctId, }); } \ No newline at end of file diff --git a/packages/web/src/lib/posthogEvents.ts b/packages/web/src/lib/posthogEvents.ts index 0e64c509..82d21eb4 100644 --- a/packages/web/src/lib/posthogEvents.ts +++ b/packages/web/src/lib/posthogEvents.ts @@ -29,71 +29,10 @@ export type PosthogEventMap = { fileLanguages: string[], isSearchExhaustive: boolean }, - share_link_created: {}, //////////////////////////////////////////////////////////////// - wa_secret_created_success: { - key: string, - }, - wa_secret_deleted_success: { - key: string, - }, - wa_secret_deleted_fail: { - key: string, - error: string, - }, - wa_secret_created_fail: { - key: string, - error: string, - }, - wa_secret_fetch_fail: { - error: string, - }, - ////////////////////////////////////////////////////////////////// - wa_warning_nav_connection_fetch_fail: { - error: string, - }, - wa_warning_nav_hover: {}, - wa_warning_nav_pressed: {}, - wa_warning_nav_connection_pressed: {}, - ////////////////////////////////////////////////////////////////// - wa_error_nav_connection_fetch_fail: { - error: string, - }, - wa_error_nav_hover: {}, - wa_error_nav_pressed: {}, - wa_error_nav_job_pressed: {}, - wa_error_nav_job_fetch_fail: { - error: string, - }, - ////////////////////////////////////////////////////////////////// - wa_progress_nav_connection_fetch_fail: { - error: string, - }, - wa_progress_nav_repo_fetch_fail: { - error: string, - }, - wa_progress_nav_hover: {}, - wa_progress_nav_pressed: {}, - wa_progress_nav_job_pressed: {}, - ////////////////////////////////////////////////////////////////// wa_trial_nav_pressed: {}, wa_trial_nav_subscription_fetch_fail: { - error: string, - }, - ////////////////////////////////////////////////////////////////// - wa_connection_list_item_error_hover: {}, - wa_connection_list_item_error_pressed: {}, - wa_connection_list_item_warning_hover: {}, - wa_connection_list_item_warning_pressed: {}, - ////////////////////////////////////////////////////////////////// - wa_connection_list_item_manage_pressed: {}, - ////////////////////////////////////////////////////////////////// - wa_create_connection_success: { - type: string, - }, - wa_create_connection_fail: { - type: string, - error: string, + errorCode: string, }, ////////////////////////////////////////////////////////////////// wa_config_editor_quick_action_pressed: { @@ -101,124 +40,64 @@ export type PosthogEventMap = { type: string, }, ////////////////////////////////////////////////////////////////// - wa_secret_combobox_import_secret_pressed: { - type: string, - }, - wa_secret_combobox_import_secret_success: { - type: string, - }, - wa_secret_combobox_import_secret_fail: { - type: string, - error: string, - }, - ////////////////////////////////////////////////////////////////// wa_billing_email_updated_success: {}, wa_billing_email_updated_fail: { - error: string, - }, - wa_billing_email_fetch_fail: { - error: string, + errorCode: string, }, wa_manage_subscription_button_create_portal_session_success: {}, wa_manage_subscription_button_create_portal_session_fail: { - error: string, + errorCode: string, }, ////////////////////////////////////////////////////////////////// wa_invite_member_card_invite_success: { num_emails: number, }, wa_invite_member_card_invite_fail: { - error: string, + errorCode: string, num_emails: number, }, wa_invite_member_card_invite_cancel: { num_emails: number, }, ////////////////////////////////////////////////////////////////// - wa_onboard_skip_onboarding: { - step: string, - }, - wa_onboard_invite_team_invite_success: { - num_emails: number, - }, - wa_onboard_invite_team_invite_fail: { - error: string, - num_emails: number, - }, - wa_onboard_invite_team_skip: { - num_emails: number, - }, - ////////////////////////////////////////////////////////////////// wa_members_list_remove_member_success: {}, wa_members_list_remove_member_fail: { - error: string, + errorCode: string, }, wa_members_list_transfer_ownership_success: {}, wa_members_list_transfer_ownership_fail: { - error: string, + errorCode: string, }, wa_members_list_leave_org_success: {}, wa_members_list_leave_org_fail: { - error: string, + errorCode: string, }, ////////////////////////////////////////////////////////////////// wa_invites_list_cancel_invite_success: {}, wa_invites_list_cancel_invite_fail: { - error: string, + errorCode: string, }, wa_invites_list_copy_invite_link_success: {}, wa_invites_list_copy_invite_link_fail: {}, wa_invites_list_copy_email_success: {}, wa_invites_list_copy_email_fail: {}, ////////////////////////////////////////////////////////////////// - wa_onboard_org_create_success: {}, - wa_onboard_org_create_fail: { - error: string, - }, - ////////////////////////////////////////////////////////////////// wa_connect_code_host_button_pressed: { name: string, }, ////////////////////////////////////////////////////////////////// wa_onboard_checkout_success: {}, wa_onboard_checkout_fail: { - error: string, + errorCode: string, }, ////////////////////////////////////////////////////////////////// wa_team_upgrade_card_pressed: {}, wa_team_upgrade_checkout_success: {}, wa_team_upgrade_checkout_fail: { - error: string, + errorCode: string, }, wa_enterprise_upgrade_card_pressed: {}, ////////////////////////////////////////////////////////////////// - wa_connection_delete_success: {}, - wa_connection_delete_fail: { - error: string, - }, - ////////////////////////////////////////////////////////////////// - wa_connection_failed_status_hover: {}, - wa_connection_retry_sync_success: {}, - wa_connection_retry_sync_fail: { - error: string, - }, - ////////////////////////////////////////////////////////////////// - wa_connection_not_found_warning_displayed: {}, - wa_connection_secrets_navigation_pressed: {}, - ////////////////////////////////////////////////////////////////// - wa_connection_retry_all_failed_repos_pressed: {}, - wa_connection_retry_all_failed_repos_fetch_fail: { - error: string, - }, - wa_connection_retry_all_failed_repos_fail: {}, - wa_connection_retry_all_failed_repos_success: {}, - wa_connection_retry_all_failed_no_repos: {}, - ////////////////////////////////////////////////////////////////// - wa_repo_retry_index_success: {}, - wa_repo_retry_index_fail: { - error: string, - }, - ////////////////////////////////////////////////////////////////// wa_login_with_github: {}, wa_login_with_google: {}, wa_login_with_gitlab: {}, @@ -235,24 +114,16 @@ export type PosthogEventMap = { ////////////////////////////////////////////////////////////////// wa_org_name_updated_success: {}, wa_org_name_updated_fail: { - error: string, + errorCode: string, }, ////////////////////////////////////////////////////////////////// wa_org_domain_updated_success: {}, wa_org_domain_updated_fail: { - error: string, + errorCode: string, }, ////////////////////////////////////////////////////////////////// - wa_onboard_github_selected: {}, - wa_onboard_gitlab_selected: {}, - wa_onboard_gitea_selected: {}, - wa_onboard_gerrit_selected: {}, - wa_onboard_bitbucket_cloud_selected: {}, - wa_onboard_bitbucket_server_selected: {}, - ////////////////////////////////////////////////////////////////// wa_security_page_click: {}, ////////////////////////////////////////////////////////////////// - wa_demo_card_click: {}, wa_demo_try_card_pressed: {}, wa_share_link_created: {}, ////////////////////////////////////////////////////////////////// @@ -262,12 +133,12 @@ export type PosthogEventMap = { ////////////////////////////////////////////////////////////////// wa_requests_list_approve_request_success: {}, wa_requests_list_approve_request_fail: { - error: string, + errorCode: string, }, ////////////////////////////////////////////////////////////////// wa_requests_list_reject_request_success: {}, wa_requests_list_reject_request_fail: { - error: string, + errorCode: string, }, ////////////////////////////////////////////////////////////////// wa_api_key_created: {}, @@ -281,11 +152,6 @@ export type PosthogEventMap = { wa_chat_thread_created: {}, ////////////////////////////////////////////////////////////////// wa_demo_docs_link_pressed: {}, - wa_demo_search_context_card_pressed: { - contextType: string, - contextName: string, - contextDisplayName: string, - }, wa_demo_search_example_card_pressed: { exampleTitle: string, exampleUrl: string, 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;