Skip to content

Commit d55c23c

Browse files
committed
feat(telemetry include referralSource in identify and clear cookie for Vercel integration
Add optional referralSource handling across auth flows and telemetry to record where new users originate. Extend telemetry.user.identify to accept referralSource and consolidate identify properties into a single object before sending to PostHog. Invoke this when users sign in via GitHub or magic link: read referralSource cookie, attach it for users created within the last 30 seconds (treating them as new users), and clear the cookie after use. This improves attribution for signups by capturing and sending referral information for new users while ensuring the referral cookie is removed after consumption.
1 parent 5aca411 commit d55c23c

File tree

8 files changed

+184
-19
lines changed

8 files changed

+184
-19
lines changed

apps/webapp/app/routes/auth.github.callback.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { getSession, redirectWithErrorMessage } from "~/models/message.server";
55
import { authenticator } from "~/services/auth.server";
66
import { setLastAuthMethodHeader } from "~/services/lastAuthMethod.server";
77
import { commitSession } from "~/services/sessionStorage.server";
8+
import { getReferralSource, clearReferralSourceCookie } from "~/services/referralSource.server";
9+
import { telemetry } from "~/services/telemetry.server";
810
import { redirectCookie } from "./auth.github";
911
import { sanitizeRedirectPath } from "~/utils";
1012

@@ -56,5 +58,28 @@ export let loader: LoaderFunction = async ({ request }) => {
5658
headers.append("Set-Cookie", await commitSession(session));
5759
headers.append("Set-Cookie", await setLastAuthMethodHeader("github"));
5860

61+
// Read referral source cookie and set in PostHog if present (only for new users), then clear it
62+
const referralSource = await getReferralSource(request);
63+
if (referralSource) {
64+
const user = await prisma.user.findUnique({
65+
where: { id: auth.userId },
66+
});
67+
if (user) {
68+
// Only set referralSource for new users (created within the last 30 seconds)
69+
const userAge = Date.now() - user.createdAt.getTime();
70+
const isNewUser = userAge < 30 * 1000; // 30 seconds
71+
72+
if (isNewUser) {
73+
telemetry.user.identify({
74+
user,
75+
isNewUser: true,
76+
referralSource,
77+
});
78+
}
79+
}
80+
// Clear the cookie after using it (regardless of whether we set it)
81+
headers.append("Set-Cookie", await clearReferralSourceCookie());
82+
}
83+
5984
return redirect(redirectTo, { headers });
6085
};

apps/webapp/app/routes/auth.google.callback.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { getSession, redirectWithErrorMessage } from "~/models/message.server";
55
import { authenticator } from "~/services/auth.server";
66
import { setLastAuthMethodHeader } from "~/services/lastAuthMethod.server";
77
import { commitSession } from "~/services/sessionStorage.server";
8+
import { getReferralSource, clearReferralSourceCookie } from "~/services/referralSource.server";
9+
import { telemetry } from "~/services/telemetry.server";
810
import { redirectCookie } from "./auth.google";
911
import { sanitizeRedirectPath } from "~/utils";
1012

@@ -56,6 +58,29 @@ export let loader: LoaderFunction = async ({ request }) => {
5658
headers.append("Set-Cookie", await commitSession(session));
5759
headers.append("Set-Cookie", await setLastAuthMethodHeader("google"));
5860

61+
// Read referral source cookie and set in PostHog if present (only for new users), then clear it
62+
const referralSource = await getReferralSource(request);
63+
if (referralSource) {
64+
const user = await prisma.user.findUnique({
65+
where: { id: auth.userId },
66+
});
67+
if (user) {
68+
// Only set referralSource for new users (created within the last 30 seconds)
69+
const userAge = Date.now() - user.createdAt.getTime();
70+
const isNewUser = userAge < 30 * 1000; // 30 seconds
71+
72+
if (isNewUser) {
73+
telemetry.user.identify({
74+
user,
75+
isNewUser: true,
76+
referralSource,
77+
});
78+
}
79+
}
80+
// Clear the cookie after using it (regardless of whether we set it)
81+
headers.append("Set-Cookie", await clearReferralSourceCookie());
82+
}
83+
5984
return redirect(redirectTo, { headers });
6085
};
6186

apps/webapp/app/routes/callback.vercel.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import { env } from "~/env.server";
66
import { redirectWithErrorMessage } from "~/models/message.server";
77
import { VercelIntegrationRepository } from "~/models/vercelIntegration.server";
88
import { logger } from "~/services/logger.server";
9-
import { requireUserId } from "~/services/session.server";
9+
import { getUserId, requireUserId } from "~/services/session.server";
10+
import { setReferralSourceCookie } from "~/services/referralSource.server";
1011
import { requestUrl } from "~/utils/requestUrl.server";
1112
import { v3ProjectSettingsPath } from "~/utils/pathBuilder";
1213
import { validateVercelOAuthState } from "~/v3/vercel/vercelOAuthState.server";
@@ -56,7 +57,23 @@ export async function loader({ request }: LoaderFunctionArgs) {
5657
return new Response("Method Not Allowed", { status: 405 });
5758
}
5859

59-
const userId = await requireUserId(request);
60+
// Check if user is authenticated
61+
const userId = await getUserId(request);
62+
63+
// If not authenticated, set referral source cookie and redirect to login
64+
if (!userId) {
65+
const currentUrl = new URL(request.url);
66+
const redirectTo = `${currentUrl.pathname}${currentUrl.search}`;
67+
const referralCookie = await setReferralSourceCookie("vercel");
68+
69+
const headers = new Headers();
70+
headers.append("Set-Cookie", referralCookie);
71+
72+
throw redirect(`/login?redirectTo=${encodeURIComponent(redirectTo)}`, { headers });
73+
}
74+
75+
// User is authenticated, proceed with OAuth callback
76+
const authenticatedUserId = await requireUserId(request);
6077

6178
const url = requestUrl(request);
6279
const parsedParams = VercelCallbackSchema.safeParse(Object.fromEntries(url.searchParams));
@@ -107,7 +124,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
107124
organization: {
108125
members: {
109126
some: {
110-
userId,
127+
userId: authenticatedUserId,
111128
},
112129
},
113130
},
@@ -120,7 +137,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
120137
if (!project) {
121138
logger.error("Project not found or user does not have access", {
122139
projectId: stateData.projectId,
123-
userId,
140+
userId: authenticatedUserId,
124141
});
125142
throw new Response("Project not found", { status: 404 });
126143
}

apps/webapp/app/routes/login.mfa/route.tsx

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ import { MultiFactorAuthenticationService } from "~/services/mfa/multiFactorAuth
2626
import { redirectWithErrorMessage, redirectBackWithErrorMessage } from "~/models/message.server";
2727
import { ServiceValidationError } from "~/v3/services/baseService.server";
2828
import { checkMfaRateLimit, MfaRateLimitError } from "~/services/mfa/mfaRateLimiter.server";
29+
import { getReferralSource, clearReferralSourceCookie } from "~/services/referralSource.server";
30+
import { telemetry } from "~/services/telemetry.server";
31+
import { prisma } from "~/db.server";
2932

3033
export const meta: MetaFunction = ({ matches }) => {
3134
const parentMeta = matches
@@ -160,11 +163,33 @@ async function completeLogin(request: Request, session: Session, userId: string)
160163
session.unset("pending-mfa-user-id");
161164
session.unset("pending-mfa-redirect-to");
162165

163-
return redirect(redirectTo, {
164-
headers: {
165-
"Set-Cookie": await sessionStorage.commitSession(authSession),
166-
},
167-
});
166+
const headers = new Headers();
167+
headers.append("Set-Cookie", await sessionStorage.commitSession(authSession));
168+
169+
// Read referral source cookie and set in PostHog if present (only for new users), then clear it
170+
const referralSource = await getReferralSource(request);
171+
if (referralSource) {
172+
const user = await prisma.user.findUnique({
173+
where: { id: userId },
174+
});
175+
if (user) {
176+
// Only set referralSource for new users (created within the last 30 seconds)
177+
const userAge = Date.now() - user.createdAt.getTime();
178+
const isNewUser = userAge < 30 * 1000; // 30 seconds
179+
180+
if (isNewUser) {
181+
telemetry.user.identify({
182+
user,
183+
isNewUser: true,
184+
referralSource,
185+
});
186+
}
187+
}
188+
// Clear the cookie after using it (regardless of whether we set it)
189+
headers.append("Set-Cookie", await clearReferralSourceCookie());
190+
}
191+
192+
return redirect(redirectTo, { headers });
168193
}
169194

170195
export default function LoginMfaPage() {

apps/webapp/app/routes/magic.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { authenticator } from "~/services/auth.server";
66
import { setLastAuthMethodHeader } from "~/services/lastAuthMethod.server";
77
import { getRedirectTo } from "~/services/redirectTo.server";
88
import { commitSession, getSession } from "~/services/sessionStorage.server";
9+
import { getReferralSource, clearReferralSourceCookie } from "~/services/referralSource.server";
10+
import { telemetry } from "~/services/telemetry.server";
911

1012
export async function loader({ request }: LoaderFunctionArgs) {
1113
const redirectTo = await getRedirectTo(request);
@@ -53,5 +55,28 @@ export async function loader({ request }: LoaderFunctionArgs) {
5355
headers.append("Set-Cookie", await commitSession(session));
5456
headers.append("Set-Cookie", await setLastAuthMethodHeader("email"));
5557

58+
// Read referral source cookie and set in PostHog if present (only for new users), then clear it
59+
const referralSource = await getReferralSource(request);
60+
if (referralSource) {
61+
const user = await prisma.user.findUnique({
62+
where: { id: auth.userId },
63+
});
64+
if (user) {
65+
// Only set referralSource for new users (created within the last 30 seconds)
66+
const userAge = Date.now() - user.createdAt.getTime();
67+
const isNewUser = userAge < 30 * 1000; // 30 seconds
68+
69+
if (isNewUser) {
70+
telemetry.user.identify({
71+
user,
72+
isNewUser: true,
73+
referralSource,
74+
});
75+
}
76+
}
77+
// Clear the cookie after using it (regardless of whether we set it)
78+
headers.append("Set-Cookie", await clearReferralSourceCookie());
79+
}
80+
5681
return redirect(redirectTo ?? "/", { headers });
5782
}

apps/webapp/app/services/postAuth.server.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,8 @@ export async function postAuthentication({
1010
loginMethod: User["authenticationMethod"];
1111
isNewUser: boolean;
1212
}) {
13-
telemetry.user.identify({ user, isNewUser });
13+
telemetry.user.identify({
14+
user,
15+
isNewUser,
16+
});
1417
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { createCookie } from "@remix-run/node";
2+
import { env } from "~/env.server";
3+
4+
export type ReferralSource = "vercel";
5+
6+
// Cookie that persists for 1 hour to track referral source during login flow
7+
export const referralSourceCookie = createCookie("referral-source", {
8+
maxAge: 60 * 60, // 1 hour
9+
httpOnly: true,
10+
sameSite: "lax",
11+
secure: env.NODE_ENV === "production",
12+
});
13+
14+
export async function getReferralSource(request: Request): Promise<ReferralSource | null> {
15+
const cookie = request.headers.get("Cookie");
16+
const value = await referralSourceCookie.parse(cookie);
17+
if (value === "vercel") {
18+
return value;
19+
}
20+
return null;
21+
}
22+
23+
export async function setReferralSourceCookie(source: ReferralSource): Promise<string> {
24+
return referralSourceCookie.serialize(source);
25+
}
26+
27+
export async function clearReferralSourceCookie(): Promise<string> {
28+
return referralSourceCookie.serialize("", {
29+
maxAge: 0,
30+
});
31+
}

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

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,32 @@ class Telemetry {
2828
}
2929

3030
user = {
31-
identify: ({ user, isNewUser }: { user: User; isNewUser: boolean }) => {
31+
identify: ({
32+
user,
33+
isNewUser,
34+
referralSource,
35+
}: {
36+
user: User;
37+
isNewUser: boolean;
38+
referralSource?: string;
39+
}) => {
3240
if (this.#posthogClient) {
41+
const properties: Record<string, any> = {
42+
email: user.email,
43+
name: user.name,
44+
authenticationMethod: user.authenticationMethod,
45+
admin: user.admin,
46+
createdAt: user.createdAt,
47+
isNewUser,
48+
};
49+
50+
if (referralSource) {
51+
properties.referralSource = referralSource;
52+
}
53+
3354
this.#posthogClient.identify({
3455
distinctId: user.id,
35-
properties: {
36-
email: user.email,
37-
name: user.name,
38-
authenticationMethod: user.authenticationMethod,
39-
admin: user.admin,
40-
createdAt: user.createdAt,
41-
isNewUser,
42-
},
56+
properties,
4357
});
4458
}
4559
if (isNewUser) {

0 commit comments

Comments
 (0)