chore(web): PostHog telemetry improvements (#672)

This commit is contained in:
Brendan Kellam 2025-12-12 12:40:47 -08:00 committed by GitHub
parent 095474a901
commit c41087acdf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 101 additions and 184 deletions

View file

@ -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;
}

View file

@ -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({

View file

@ -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({

View file

@ -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 {

View file

@ -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({

View file

@ -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({

View file

@ -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({

View file

@ -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}
>
<ThemeProvider
attribute="class"

View file

@ -34,9 +34,17 @@ interface PostHogProviderProps {
children: React.ReactNode
isDisabled: boolean
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();
useEffect(() => {
@ -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 (

View file

@ -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)

View file

@ -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', {});

View file

@ -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 {

View file

@ -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);

View file

@ -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<E extends PosthogEvent>(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);
}
/**

View file

@ -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<RequestCookies, 'get'>): 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<E extends PosthogEvent>(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<E extends PosthogEvent>(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,
});
}

View file

@ -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,

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.
*/
const getVerifiedApiObject = async (apiKeyString: string): Promise<ApiKey | undefined> => {
export const getVerifiedApiObject = async (apiKeyString: string): Promise<ApiKey | undefined> => {
const parts = apiKeyString.split("-");
if (parts.length !== 2 || parts[0] !== "sourcebot") {
return undefined;