Skip to content
Merged
35 changes: 35 additions & 0 deletions src/app/users/after-sign-in/route.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
import { getProfileRedirectPath, getUserFromAuth } from '@/lib/user.server';
import { isValidCallbackPath } from '@/lib/getSignInCallbackUrl';
import { maybeInterceptWithSurvey } from '@/lib/survey-redirect';
import PostHogClient from '@/lib/posthog';
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { APP_URL } from '@/lib/constants';

/**
* Resolves a product identifier from the signup entry point. Returns null when
* the entry point is generic (e.g. /get-started, /profile) so we leave the
* property unset rather than guessing.
*/
function resolveSignupProduct(callbackPath: string | null, hasSource: boolean): string | null {
if (hasSource) return 'kilo-code'; // IDE install flow
if (!callbackPath) return null;
if (callbackPath.startsWith('/claw')) return 'kiloclaw';
if (callbackPath.startsWith('/cloud')) return 'cloud-agent';
if (callbackPath.startsWith('/code-reviews')) return 'code-reviews';
if (callbackPath.startsWith('/app-builder')) return 'app-builder';
if (callbackPath.startsWith('/install')) return 'kilo-code';
return null;
}

export async function GET(request: NextRequest) {
const url = new URL(request.url);
const { user } = await getUserFromAuth({ adminOnly: false, DANGEROUS_allowBlockedUsers: true });
Expand All @@ -27,6 +44,24 @@ export async function GET(request: NextRequest) {
}

if (user.has_validation_stytch === null) {
// New user: stamp which product they signed up for. Derived from
// responsePath (the already-validated destination) rather than the raw
// callbackPath query param, so the value cannot be user-tampered. This
// runs exactly once per signup because has_validation_stytch is set
// after account verification completes.
const product = resolveSignupProduct(responsePath, !!url.searchParams.get('source'));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When there's no callbackPath and no source, responsePath falls through to getProfileRedirectPath(user). If that ever returns a product prefixed path (e.g. /claw/...) for a brand-new user, they'd be misattributed even though they signed up generically.

Worth confirming what getProfileRedirectPath returns for users with has_validation_stytch === null. If there's any risk, one option is to only resolve the product when a callbackPath was explicitly provided:

const product = resolveSignupProduct(
  callbackPath && isValidCallbackPath(callbackPath) ? responsePath : null,
  !!url.searchParams.get('source'),
);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just seeing this. I'm on it

if (product) {
PostHogClient().capture({
distinctId: user.google_user_email,
event: 'signup_product_attributed',
properties: {
first_product_signup: product,
signup_destination: responsePath,
$set_once: { first_product_signup: product },
},
});
}

// For new users needing verification, only pass callbackPath if explicitly provided.
// Otherwise, account-verification will redirect to /get-started by default.
if (callbackPath && isValidCallbackPath(callbackPath)) {
Expand Down
Loading