Skip to content

Commit bb81ce3

Browse files
committed
feat(analytics): add PostHog tracking to Vercel onboarding
Introduce PostHog instrumentation in the VercelOnboardingModal to capture user progress and actions during the onboarding flow. Key changes: - Import posthog and add a reusable trackOnboarding callback that attaches context (origin, step, organization_slug, project_slug). - Start session recording and emit a "vercel onboarding started" event the first time the modal reaches project-selection while open. - Track key milestones and user decisions: - "vercel onboarding project selected" when a project is chosen. - "vercel onboarding abandoned" when the user skips onboarding. - "vercel onboarding env mapping completed" with skipped flag or selected staging environment. - "vercel onboarding build settings completed". - "vercel onboarding completed" with github_connected flag. - Ensure tracking callbacks are included in effect and callback deps to avoid stale closures and double-tracking. Why: - Provide visibility into onboarding funnel performance and user behaviors to improve onboarding UX and conversion.
1 parent 349feb0 commit bb81ce3

File tree

2 files changed

+87
-9
lines changed

2 files changed

+87
-9
lines changed

apps/webapp/app/components/integrations/VercelOnboardingModal.tsx

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import { type VercelOnboardingData } from "~/presenters/v3/VercelSettingsPresent
4747
import { vercelAppInstallPath, v3ProjectSettingsPath, githubAppInstallPath, vercelResourcePath } from "~/utils/pathBuilder";
4848
import type { loader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel";
4949
import { useEffect, useState, useCallback, useRef } from "react";
50+
import { usePostHogTracking } from "~/hooks/usePostHog";
5051

5152
function safeRedirectUrl(url: string): string | null {
5253
try {
@@ -114,6 +115,7 @@ export function VercelOnboardingModal({
114115
nextUrl?: string;
115116
onDataReload?: (vercelStagingEnvironment?: string) => void;
116117
}) {
118+
const { capture, startSessionRecording } = usePostHogTracking();
117119
const navigation = useNavigation();
118120
const fetcher = useTypedFetcher<typeof loader>();
119121
const envMappingFetcher = useFetcher();
@@ -172,6 +174,31 @@ export function VercelOnboardingModal({
172174
prevIsOpenRef.current = isOpen;
173175
}, [isOpen, state, computeInitialState]);
174176

177+
const trackOnboarding = useCallback(
178+
(eventName: string, extraProperties?: Record<string, unknown>) => {
179+
capture(eventName, {
180+
origin: fromMarketplaceContext ? "marketplace" : "dashboard",
181+
step: state,
182+
organization_slug: organizationSlug,
183+
project_slug: projectSlug,
184+
...extraProperties,
185+
});
186+
},
187+
[capture, fromMarketplaceContext, state, organizationSlug, projectSlug]
188+
);
189+
190+
const hasTrackedStartRef = useRef(false);
191+
useEffect(() => {
192+
if (isOpen && state === "project-selection" && !hasTrackedStartRef.current) {
193+
hasTrackedStartRef.current = true;
194+
startSessionRecording();
195+
trackOnboarding("vercel onboarding started");
196+
}
197+
if (!isOpen) {
198+
hasTrackedStartRef.current = false;
199+
}
200+
}, [isOpen, state, trackOnboarding, startSessionRecording]);
201+
175202
const [selectedVercelProject, setSelectedVercelProject] = useState<{
176203
id: string;
177204
name: string;
@@ -337,14 +364,17 @@ export function VercelOnboardingModal({
337364

338365
useEffect(() => {
339366
if (state === "project-selection" && fetcher.data && "success" in fetcher.data && fetcher.data.success && fetcher.state === "idle") {
367+
trackOnboarding("vercel onboarding project selected", {
368+
vercel_project_name: selectedVercelProject?.name,
369+
});
340370
setState("loading-env-mapping");
341371
if (onDataReload) {
342372
onDataReload();
343373
}
344374
} else if (fetcher.data && "error" in fetcher.data && typeof fetcher.data.error === "string") {
345375
setProjectSelectionError(fetcher.data.error);
346376
}
347-
}, [state, fetcher.data, fetcher.state, onDataReload]);
377+
}, [state, fetcher.data, fetcher.state, onDataReload, trackOnboarding, selectedVercelProject?.name]);
348378

349379
// For marketplace origin, skip env-mapping step
350380
useEffect(() => {
@@ -437,6 +467,7 @@ export function VercelOnboardingModal({
437467
}, [selectedVercelProject, fetcher, actionUrl]);
438468

439469
const handleSkipOnboarding = useCallback(() => {
470+
trackOnboarding("vercel onboarding abandoned");
440471
onClose();
441472

442473
if (fromMarketplaceContext) {
@@ -449,14 +480,23 @@ export function VercelOnboardingModal({
449480
method: "post",
450481
action: actionUrl,
451482
});
452-
}, [actionUrl, fetcher, onClose, nextUrl, fromMarketplaceContext]);
483+
}, [actionUrl, fetcher, onClose, nextUrl, fromMarketplaceContext, trackOnboarding]);
453484

454485
const handleSkipEnvMapping = useCallback(() => {
486+
trackOnboarding("vercel onboarding env mapping completed", {
487+
skipped: true,
488+
staging_environment: null,
489+
});
455490
setVercelStagingEnvironment(null);
456491
setState("loading-env-vars");
457-
}, []);
492+
}, [trackOnboarding]);
458493

459494
const handleUpdateEnvMapping = useCallback(() => {
495+
trackOnboarding("vercel onboarding env mapping completed", {
496+
skipped: false,
497+
staging_environment: vercelStagingEnvironment?.displayName ?? null,
498+
});
499+
460500
if (!vercelStagingEnvironment) {
461501
setState("loading-env-vars");
462502
return;
@@ -471,9 +511,11 @@ export function VercelOnboardingModal({
471511
action: actionUrl,
472512
});
473513

474-
}, [vercelStagingEnvironment, envMappingFetcher, actionUrl]);
514+
}, [vercelStagingEnvironment, envMappingFetcher, actionUrl, trackOnboarding]);
475515

476516
const handleBuildSettingsNext = useCallback(() => {
517+
trackOnboarding("vercel onboarding build settings completed");
518+
477519
if (nextUrl && fromMarketplaceContext && isGitHubConnectedForOnboarding) {
478520
setIsRedirecting(true);
479521
}
@@ -501,7 +543,7 @@ export function VercelOnboardingModal({
501543
if (!isGitHubConnectedForOnboarding) {
502544
setState("github-connection");
503545
}
504-
}, [vercelStagingEnvironment, pullEnvVarsBeforeBuild, atomicBuilds, discoverEnvVars, syncEnvVarsMapping, nextUrl, fromMarketplaceContext, isGitHubConnectedForOnboarding, completeOnboardingFetcher, actionUrl]);
546+
}, [vercelStagingEnvironment, pullEnvVarsBeforeBuild, atomicBuilds, discoverEnvVars, syncEnvVarsMapping, nextUrl, fromMarketplaceContext, isGitHubConnectedForOnboarding, completeOnboardingFetcher, actionUrl, trackOnboarding]);
505547

506548
const handleFinishOnboarding = useCallback((e: React.FormEvent<HTMLFormElement>) => {
507549
e.preventDefault();
@@ -531,9 +573,12 @@ export function VercelOnboardingModal({
531573

532574
useEffect(() => {
533575
if (state === "completed") {
576+
trackOnboarding("vercel onboarding completed", {
577+
github_connected: isGitHubConnectedForOnboarding,
578+
});
534579
onClose();
535580
}
536-
}, [state, onClose]);
581+
}, [state, onClose, trackOnboarding, isGitHubConnectedForOnboarding]);
537582

538583
useEffect(() => {
539584
if (state === "installing") {
@@ -584,7 +629,14 @@ export function VercelOnboardingModal({
584629

585630
if (isLoadingState) {
586631
return (
587-
<Dialog open={isOpen} onOpenChange={(open) => !open && !fromMarketplaceContext && onClose()}>
632+
<Dialog open={isOpen} onOpenChange={(open) => {
633+
if (!open && !fromMarketplaceContext) {
634+
if (state as string !== "completed") {
635+
trackOnboarding("vercel onboarding abandoned");
636+
}
637+
onClose();
638+
}
639+
}}>
588640
<DialogContent className="max-w-lg">
589641
<DialogHeader>
590642
<div className="flex items-center gap-2">
@@ -607,7 +659,14 @@ export function VercelOnboardingModal({
607659
const showGitHubConnection = state === "github-connection";
608660

609661
return (
610-
<Dialog open={isOpen} onOpenChange={(open) => !open && !fromMarketplaceContext && onClose()}>
662+
<Dialog open={isOpen} onOpenChange={(open) => {
663+
if (!open && !fromMarketplaceContext) {
664+
if (state !== "completed") {
665+
trackOnboarding("vercel onboarding abandoned");
666+
}
667+
onClose();
668+
}
669+
}}>
611670
<DialogContent className="max-w-lg">
612671
<DialogHeader>
613672
<div className="flex items-center gap-2">
@@ -902,6 +961,10 @@ export function VercelOnboardingModal({
902961
<Button
903962
variant="primary/medium"
904963
onClick={() => {
964+
trackOnboarding("vercel onboarding env vars configured", {
965+
env_vars_enabled: enabledEnvVars.length,
966+
env_vars_total: syncableEnvVars.length,
967+
});
905968
if (fromMarketplaceContext) {
906969
handleBuildSettingsNext();
907970
} else {

apps/webapp/app/hooks/usePostHog.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useLocation } from "@remix-run/react";
22
import posthog from "posthog-js";
3-
import { useEffect, useRef } from "react";
3+
import { useCallback, useEffect, useRef } from "react";
44
import { useOrganizationChanged } from "./useOrganizations";
55
import { useOptionalUser, useUserChanged } from "./useUser";
66
import { useProjectChanged } from "./useProject";
@@ -68,3 +68,18 @@ export const usePostHog = (apiKey?: string, logging = false, debug = false): voi
6868
posthog.capture("$pageview");
6969
}, [location, logging]);
7070
};
71+
72+
export function usePostHogTracking() {
73+
const capture = useCallback(
74+
(eventName: string, properties?: Record<string, unknown>) => {
75+
posthog.capture(eventName, properties);
76+
},
77+
[]
78+
);
79+
80+
const startSessionRecording = useCallback(() => {
81+
posthog.startSessionRecording();
82+
}, []);
83+
84+
return { capture, startSessionRecording };
85+
}

0 commit comments

Comments
 (0)