feat: store ads clickid when creating an account (#25763)

* feat: store ads clickid when creating an account

* fix: type check

* fix: google campaignId not being added to stripe

* remove tracking in stripe app
This commit is contained in:
Amit Sharma
2025-12-22 23:29:51 +05:30
committed by GitHub
parent b664857809
commit f5b9b6b0f9
7 changed files with 79 additions and 19 deletions
+4 -1
View File
@@ -509,4 +509,7 @@ SEED_PLATFORM_OAUTH_CLIENT_SECRET=
ENABLE_ASYNC_TASKER="false" # set to "true" to enable
TRIGGER_SECRET_KEY=
TRIGGER_API_URL=https://api.trigger.dev
TRIGGER_DEV_PROJECT_REF=
TRIGGER_DEV_PROJECT_REF=
GOOGLE_ADS_ENABLED=1 # To enable Google Ads tracking (gclid)
LINKEDIN_ADS_ENABLED=1 # To enable LinkedIn Ads tracking (li_fat_id)
@@ -2,12 +2,14 @@ import type { GetServerSidePropsContext } from "next";
import { getServerSession } from "@calcom/feature-auth/lib/getServerSession";
import { getOptions } from "@calcom/feature-auth/lib/next-auth-options";
import { getTrackingFromCookies } from "@calcom/lib/tracking";
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const session = await getServerSession({
req: context.req,
authOptions: getOptions({
getDubId: () => context.req.cookies.dub_id || context.req.cookies.dclid,
getTrackingData: () => getTrackingFromCookies(context.req.cookies),
}),
});
// Disable this check if we ever make this self serve.
+9 -1
View File
@@ -2,9 +2,17 @@ import type { NextApiRequest, NextApiResponse } from "next";
import NextAuth from "next-auth";
import { getOptions } from "@calcom/features/auth/lib/next-auth-options";
import { getTrackingFromCookies } from "@calcom/lib/tracking";
// pass req to NextAuth: https://github.com/nextauthjs/next-auth/discussions/469
const handler = (req: NextApiRequest, res: NextApiResponse) =>
NextAuth(req, res, getOptions({ getDubId: () => req.cookies.dub_id || req.cookies.dclid }));
NextAuth(
req,
res,
getOptions({
getDubId: () => req.cookies.dub_id || req.cookies.dclid,
getTrackingData: () => getTrackingFromCookies(req.cookies),
})
);
export default handler;
@@ -5,7 +5,6 @@ import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/li
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { checkPremiumUsername } from "@calcom/features/ee/common/lib/checkPremiumUsername";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { getTrackingFromCookies } from "@calcom/lib/tracking";
import prisma from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
@@ -40,8 +39,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return;
}
const tracking = getTrackingFromCookies(req.cookies);
const return_url = `${WEBAPP_URL}/api/integrations/stripepayment/paymentCallback?checkoutSessionId={CHECKOUT_SESSION_ID}&callbackUrl=${callbackUrl}`;
const createSessionParams: Stripe.Checkout.SessionCreateParams = {
mode: "subscription",
@@ -58,8 +55,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
metadata: {
userId: userId.toString(),
intentUsername,
...(tracking?.googleAds?.gclid && { gclid: tracking.googleAds.gclid, campaignId: tracking.googleAds.campaignId }),
...(tracking?.linkedInAds?.liFatId && { liFatId: tracking.linkedInAds.liFatId, linkedInCampaignId: tracking.linkedInAds?.campaignId }),
},
};
@@ -12,7 +12,9 @@ import GoogleProvider from "next-auth/providers/google";
import { updateProfilePhotoGoogle } from "@calcom/app-store/_utils/oauth/updateProfilePhotoGoogle";
import GoogleCalendarService from "@calcom/app-store/googlecalendar/lib/CalendarService";
import { LicenseKeySingleton } from "@calcom/ee/common/server/LicenseKeyService";
import { getBillingProviderService } from "@calcom/features/ee/billing/di/containers/Billing";
import { CredentialRepository } from "@calcom/features/credentials/repositories/CredentialRepository";
import type { TrackingData } from "@calcom/lib/tracking";
import { DeploymentRepository } from "@calcom/features/ee/deployment/repositories/DeploymentRepository";
import createUsersAndConnectToOrg from "@calcom/features/ee/dsync/lib/users/createUsersAndConnectToOrg";
import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
@@ -456,9 +458,12 @@ const mapIdentityProvider = (providerName: string) => {
export const getOptions = ({
getDubId,
getTrackingData,
}: {
/** so we can extract the Dub cookie in both pages and app routers */
getDubId: () => string | undefined;
/** Ad tracking data for Stripe customer metadata */
getTrackingData: () => TrackingData;
}): AuthOptions => ({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
@@ -596,14 +601,14 @@ export const getOptions = ({
org:
profileOrg && !profileOrg.isPlatform
? {
id: profileOrg.id,
name: profileOrg.name,
slug: profileOrg.slug ?? profileOrg.requestedSlug ?? "",
logoUrl: profileOrg.logoUrl,
fullDomain: getOrgFullOrigin(profileOrg.slug ?? profileOrg.requestedSlug ?? ""),
domainSuffix: subdomainSuffix(),
role: orgRole as MembershipRole, // It can't be undefined if we have a profileOrg
}
id: profileOrg.id,
name: profileOrg.name,
slug: profileOrg.slug ?? profileOrg.requestedSlug ?? "",
logoUrl: profileOrg.logoUrl,
fullDomain: getOrgFullOrigin(profileOrg.slug ?? profileOrg.requestedSlug ?? ""),
domainSuffix: subdomainSuffix(),
role: orgRole as MembershipRole, // It can't be undefined if we have a profileOrg
}
: null,
} as JWT;
};
@@ -1066,11 +1071,12 @@ export const getOptions = ({
const { orgUsername, orgId } = await checkIfUserShouldBelongToOrg(idP, user.email);
try {
const newUsername = orgId ? slugify(orgUsername) : usernameSlug(user.name);
const newUser = await prisma.user.create({
data: {
// Slugify the incoming name and append a few random characters to
// prevent conflicts for users with the same name.
username: orgId ? slugify(orgUsername) : usernameSlug(user.name),
username: newUsername,
emailVerified: new Date(Date.now()),
name: user.name,
...(user.image && { avatarUrl: user.image }),
@@ -1094,6 +1100,40 @@ export const getOptions = ({
);
await calcomAdapter.linkAccount(linkAccountNewUserData);
waitUntil(
(async () => {
try {
const tracking = getTrackingData();
const billingService = getBillingProviderService();
const customer = await billingService.createCustomer({
email: newUser.email,
metadata: {
email: newUser.email,
username: newUser.username ?? newUsername,
...(tracking.googleAds?.gclid && {
gclid: tracking.googleAds.gclid,
campaignId: tracking.googleAds.campaignId,
}),
...(tracking.linkedInAds?.liFatId && {
liFatId: tracking.linkedInAds.liFatId,
linkedInCampaignId: tracking.linkedInAds.campaignId,
}),
},
});
await prisma.user.update({
where: { id: newUser.id },
data: {
metadata: {
stripeCustomerId: customer.stripeCustomerId,
},
},
});
} catch (err) {
log.error("Failed to create Stripe customer with tracking", err);
}
})()
);
if (account.twoFactorEnabled) {
return loginWithTotp(newUser.email);
} else {
@@ -16,6 +16,7 @@ import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import type { CustomNextApiHandler } from "@calcom/lib/server/username";
import { usernameHandler } from "@calcom/lib/server/username";
import { getTrackingFromCookies } from "@calcom/lib/tracking";
import { prisma } from "@calcom/prisma";
import { CreationSource } from "@calcom/prisma/enums";
import { IdentityProvider } from "@calcom/prisma/enums";
@@ -105,13 +106,24 @@ const handler: CustomNextApiHandler = async (body, usernameStatus) => {
username = usernameAndEmailValidation.username;
}
// Create the customer in Stripe
// Create the customer in Stripe with ad tracking metadata
const cookieStore = await cookies();
const cookiesObj = Object.fromEntries(cookieStore.getAll().map((c) => [c.name, c.value]));
const tracking = getTrackingFromCookies(cookiesObj);
const customer = await billingService.createCustomer({
email,
metadata: {
email /* Stripe customer email can be changed, so we add this to keep track of which email was used to signup */,
username,
...(tracking.googleAds?.gclid && {
gclid: tracking.googleAds.gclid,
campaignId: tracking.googleAds.campaignId,
}),
...(tracking.linkedInAds?.liFatId && {
liFatId: tracking.linkedInAds.liFatId,
linkedInCampaignId: tracking.linkedInAds.campaignId,
}),
},
});
+2 -2
View File
@@ -24,14 +24,14 @@ export function getTrackingFromCookies(cookies?: NextApiRequest["cookies"]): Tra
if (process.env.GOOGLE_ADS_ENABLED === "1" && cookies.gclid) {
tracking.googleAds = {
gclid: cookies.gclid,
...(cookies.gad_campaignid && { campaignId: cookies.gad_campaignid }),
...(cookies.gad_campaignId && { campaignId: cookies.gad_campaignId }),
};
}
if (process.env.LINKEDIN_ADS_ENABLED === "1" && cookies.li_fat_id) {
tracking.linkedInAds = {
liFatId: cookies.li_fat_id,
...(cookies.li_campaignid && { campaignId: cookies.li_campaignid }),
...(cookies.li_campaignId && { campaignId: cookies.li_campaignId }),
};
}