Skip to content

Commit 0fe9c5e

Browse files
D-K-PTrigger.dev RepoOps
authored andcommitted
feat(webapp): upgrade posthog-js and capture onboarding, organization, and project analytics
Upgrade posthog-js in the webapp from 1.93.3 to 1.407.3. The `posthog.init` options pin the client defaults that changed between these versions — person profiles, persistence, heatmaps, dead-click capture, exception capture, web-vitals capture, and conversations — so what the webapp captures is unchanged by the upgrade itself. Add server-side product-analytics for the managed cloud service: the onboarding form answers (referral source and role), an "organization created" event (with company domain and size when provided), and a "project created" event. These new events are collected on the managed cloud only — self-hosted installations continue to send the same telemetry as before. Mono-RevId: ae703b3df9c3288cc002559f6e8f75c4e2451c33
1 parent 07374a9 commit 0fe9c5e

7 files changed

Lines changed: 158 additions & 19 deletions

File tree

‎apps/webapp/app/hooks/usePostHog.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ export const usePostHog = (
2828
ui_host: uiHost,
2929
cross_subdomain_cookie: true,
3030
opt_in_site_apps: true,
31+
person_profiles: "always",
32+
persistence: "cookie",
33+
capture_heatmaps: false,
34+
capture_dead_clicks: false,
35+
capture_exceptions: false,
36+
capture_performance: { web_vitals: false },
37+
disable_conversations: true,
3138
debug,
3239
loaded: function (posthog) {
3340
if (logging) console.log("PostHog loaded");

‎apps/webapp/app/models/organization.server.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
import { buildDefaultBillingAlerts } from "~/services/billingAlertsDefaults.server";
2929
import { enqueueAttioWorkspaceSync } from "~/services/attio.server";
3030
import { logger } from "~/services/logger.server";
31+
import { telemetry } from "~/services/telemetry.server";
3132
import {
3233
applyBillingLimitPauseAfterEnvCreate,
3334
getInitialEnvPauseStateForBillingLimit,
@@ -162,6 +163,8 @@ export async function createOrganization(
162163
adminUserId: userId,
163164
});
164165

166+
telemetry.organization.new({ userId, organization });
167+
165168
// Awaited so the seed can't land after the user's first alert edit.
166169
await seedDefaultBillingAlerts(organization.id);
167170

‎apps/webapp/app/models/project.server.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { customAlphabet, nanoid } from "nanoid";
33
import slug from "slug";
44
import { $replica, prisma } from "~/db.server";
55
import { projectCreated } from "~/services/projectCreated.server";
6+
import { telemetry } from "~/services/telemetry.server";
67
import { ServiceValidationError } from "~/v3/services/common.server";
78
import {
89
type Organization,
@@ -143,6 +144,8 @@ export async function createProject(
143144

144145
await projectCreated(organization, project);
145146

147+
telemetry.project.new({ userId, organizationId: organization.id, project });
148+
146149
return project;
147150
}
148151

‎apps/webapp/app/routes/confirm-basic-details.tsx‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { useUser } from "~/hooks/useUser";
2727
import { redirectWithSuccessMessage } from "~/models/message.server";
2828
import { updateUser } from "~/models/user.server";
2929
import { requireUserId } from "~/services/session.server";
30+
import { telemetry } from "~/services/telemetry.server";
3031
import { emailSchema, MAX_EMAIL_LENGTH } from "~/utils/emailValidation";
3132
import { rootPath } from "~/utils/pathBuilder";
3233
import { getVercelInstallParams } from "~/v3/vercel";
@@ -177,6 +178,13 @@ export const action: ActionFunction = async ({ request }) => {
177178
onboardingData: Object.keys(onboardingData).length > 0 ? onboardingData : undefined,
178179
});
179180

181+
telemetry.user.onboardingDetailsSubmitted({
182+
userId,
183+
referralSource: submission.value.referralSource,
184+
referralSourceOther: submission.value.referralSourceOther,
185+
role: submission.value.role,
186+
});
187+
180188
const vercelParams = getVercelInstallParams(request);
181189
let redirectUrl = rootPath();
182190

‎apps/webapp/app/services/telemetry.server.ts‎

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,29 @@
11
import { PostHog } from "posthog-node";
2+
import { z } from "zod";
23
import { env } from "~/env.server";
34
import type { MatchedOrganization } from "~/hooks/useOrganizations";
45
import type { Organization } from "~/models/organization.server";
56
import type { Project } from "~/models/project.server";
67
import type { User } from "~/models/user.server";
8+
import { extractDomain } from "~/utils/favicon";
9+
import { featuresForUrl } from "~/features.server";
710
import { singleton } from "~/utils/singleton";
811
import { enqueueAttioUserSync } from "./attio.server";
912
import { loopsClient } from "./loops.server";
1013

14+
const OrgOnboardingDataSchema = z.object({ companyUrl: z.string().optional() });
15+
16+
// Cloud-only telemetry. Self-hosted installs must keep sending exactly what
17+
// they did before, so new events are gated on this rather than expanding what
18+
// self-hosters emit. Derived from APP_ORIGIN so it works without a request.
19+
const IS_MANAGED_CLOUD = (() => {
20+
try {
21+
return featuresForUrl(new URL(env.APP_ORIGIN)).isManagedCloud;
22+
} catch {
23+
return false;
24+
}
25+
})();
26+
1127
type Options = {
1228
postHogApiKey?: string;
1329
};
@@ -85,6 +101,37 @@ class Telemetry {
85101
});
86102
}
87103
},
104+
onboardingDetailsSubmitted: ({
105+
userId,
106+
referralSource,
107+
referralSourceOther,
108+
role,
109+
}: {
110+
userId: string;
111+
referralSource?: string;
112+
referralSourceOther?: string;
113+
role?: string;
114+
}) => {
115+
if (this.#posthogClient === undefined) return;
116+
if (!IS_MANAGED_CLOUD) return;
117+
118+
const properties: Record<string, any> = {};
119+
if (referralSource) {
120+
properties.referral_source_self_reported = referralSource;
121+
}
122+
if (referralSource === "Other" && referralSourceOther) {
123+
properties.referral_source_other = referralSourceOther;
124+
}
125+
if (role) {
126+
properties.role = role;
127+
}
128+
this.#capture({
129+
userId,
130+
event: "onboarding details submitted",
131+
eventProperties: properties,
132+
userProperties: properties,
133+
});
134+
},
88135
};
89136

90137
organization = {
@@ -99,16 +146,33 @@ class Telemetry {
99146
},
100147
});
101148
},
102-
new: ({
103-
userId,
104-
organization,
105-
organizationCount,
106-
}: {
107-
userId: string;
108-
organization: Organization;
109-
organizationCount: number;
110-
}) => {
149+
new: ({ userId, organization }: { userId: string; organization: Organization }) => {
111150
if (this.#posthogClient === undefined) return;
151+
if (!IS_MANAGED_CLOUD) return;
152+
153+
const companyProperties: Record<string, string> = {};
154+
const onboardingData = OrgOnboardingDataSchema.safeParse(organization.onboardingData);
155+
const companyDomain =
156+
onboardingData.success && onboardingData.data.companyUrl
157+
? extractDomain(onboardingData.data.companyUrl)
158+
: null;
159+
if (companyDomain) {
160+
companyProperties.company_domain = companyDomain;
161+
}
162+
if (organization.companySize) {
163+
companyProperties.company_size = organization.companySize;
164+
}
165+
166+
this.#posthogClient.groupIdentify({
167+
groupType: "organization",
168+
groupKey: organization.id,
169+
properties: {
170+
name: organization.title,
171+
slug: organization.slug,
172+
...companyProperties,
173+
},
174+
});
175+
112176
this.#capture({
113177
userId,
114178
event: "organization created",
@@ -119,9 +183,7 @@ class Telemetry {
119183
title: organization.title,
120184
createdAt: organization.createdAt,
121185
updatedAt: organization.updatedAt,
122-
},
123-
userProperties: {
124-
organizationCount: organizationCount,
186+
...companyProperties,
125187
},
126188
});
127189
},
@@ -154,10 +216,12 @@ class Telemetry {
154216
project: Project;
155217
}) => {
156218
if (this.#posthogClient === undefined) return;
219+
if (!IS_MANAGED_CLOUD) return;
157220
this.#capture({
158221
userId,
159222
event: "project created",
160223
organizationId,
224+
projectId: project.id,
161225
eventProperties: {
162226
id: project.id,
163227

‎apps/webapp/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@
180180
"parse-duration": "^2.1.0",
181181
"pg": "8.15.6",
182182
"@prisma/adapter-pg": "6.14.0",
183-
"posthog-js": "^1.93.3",
183+
"posthog-js": "1.407.3",
184184
"posthog-node": "5.35.6",
185185
"prism-react-renderer": "^2.3.1",
186186
"prismjs": "^1.30.0",

‎pnpm-lock.yaml‎

Lines changed: 60 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)