Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { ConfirmProvider } from "ui/components/shared/Confirm";
import LoadingScreen from "ui/components/shared/LoadingScreen";
import useAuthTelemetry from "ui/hooks/useAuthTelemetry";
import { bootstrapApp } from "ui/setup";
import { captureAdAttribution } from "ui/utils/adAttribution";
import { listenForAccessToken } from "ui/utils/browser";
import { useLaunchDarkly } from "ui/utils/launchdarkly";
import { InstallRouteListener } from "ui/utils/routeListener";
Expand Down Expand Up @@ -82,6 +83,10 @@ const App = ({ apiKey, ...props }: AppProps & AuthProps) => {
const router = useRouter();
let head: React.ReactNode;

useEffect(() => {
captureAdAttribution();
}, []);

// HACK: Coordinates with the recording page to render its <head> contents for
// social meta tags. This can be removed once we are able to handle SSP
// properly all the way to the pages. __N_SSP is a very private
Expand Down
87 changes: 87 additions & 0 deletions src/ui/utils/adAttribution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// persists ad-campaign click IDs and utm params captured from the URL so

Check failure on line 1 in src/ui/utils/adAttribution.ts

View workflow job for this annotation

GitHub Actions / Trunk Check

prettier

Incorrect formatting, autoformat by running 'trunk fmt'
// we can fire a segment `Created Account` event with attribution after
// the user completes the auth0 round-trip. first-touch only: once set,
// the payload survives until clearAdAttribution() wipes it (normally
// right after the recordFirstLogin mutation succeeds).

export type AdAttribution = {
li_fat_id: string | null;
twclid: string | null;
rdt_cid: string | null;
utm_source: string | null;
utm_medium: string | null;
utm_campaign: string | null;
utm_content: string | null;
utm_term: string | null;
};

export type AdAttributionInput = {
liClickId: string | null;
xClickId: string | null;
redditClickId: string | null;
utmSource: string | null;
utmMedium: string | null;
utmCampaign: string | null;
utmContent: string | null;
utmTerm: string | null;
};

const STORAGE_KEY = "replay_ad_attribution";
const AD_PARAMS = ["li_fat_id", "twclid", "rdt_cid"] as const;
const UTM_PARAMS = [
"utm_source",
"utm_medium",
"utm_campaign",
"utm_content",
"utm_term",
] as const;

export function captureAdAttribution(): void {
if (typeof window === "undefined") return;

Check failure on line 40 in src/ui/utils/adAttribution.ts

View workflow job for this annotation

GitHub Actions / Trunk Check

eslint(curly)

[new] Expected { after 'if' condition.
if (localStorage.getItem(STORAGE_KEY)) return;

Check failure on line 41 in src/ui/utils/adAttribution.ts

View workflow job for this annotation

GitHub Actions / Trunk Check

eslint(curly)

[new] Expected { after 'if' condition.

const params = new URLSearchParams(window.location.search);
const hasAny = [...AD_PARAMS, ...UTM_PARAMS].some(k => params.get(k));
if (!hasAny) return;

Check failure on line 45 in src/ui/utils/adAttribution.ts

View workflow job for this annotation

GitHub Actions / Trunk Check

eslint(curly)

[new] Expected { after 'if' condition.

const attribution: AdAttribution = {
li_fat_id: params.get("li_fat_id"),
twclid: params.get("twclid"),
rdt_cid: params.get("rdt_cid"),
utm_source: params.get("utm_source"),
utm_medium: params.get("utm_medium"),
utm_campaign: params.get("utm_campaign"),
utm_content: params.get("utm_content"),
utm_term: params.get("utm_term"),
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(attribution));
}

export function readAdAttribution(): AdAttribution | null {
if (typeof window === "undefined") return null;

Check failure on line 61 in src/ui/utils/adAttribution.ts

View workflow job for this annotation

GitHub Actions / Trunk Check

eslint(curly)

[new] Expected { after 'if' condition.
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;

Check failure on line 63 in src/ui/utils/adAttribution.ts

View workflow job for this annotation

GitHub Actions / Trunk Check

eslint(curly)

[new] Expected { after 'if' condition.
try {
return JSON.parse(raw) as AdAttribution;
} catch {
return null;
}
}

export function clearAdAttribution(): void {
if (typeof window === "undefined") return;

Check failure on line 72 in src/ui/utils/adAttribution.ts

View workflow job for this annotation

GitHub Actions / Trunk Check

eslint(curly)

[new] Expected { after 'if' condition.
localStorage.removeItem(STORAGE_KEY);
}

export function toAdAttributionInput(a: AdAttribution): AdAttributionInput {
return {
liClickId: a.li_fat_id,
xClickId: a.twclid,
redditClickId: a.rdt_cid,
utmSource: a.utm_source,
utmMedium: a.utm_medium,
utmCampaign: a.utm_campaign,
utmContent: a.utm_content,
utmTerm: a.utm_term,
};
}
27 changes: 26 additions & 1 deletion src/ui/utils/auth.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { clearAdAttribution, readAdAttribution } from "./adAttribution";
import { setAccessTokenInBrowserPrefs } from "./browser";

export function getAuthHost() {
Expand All @@ -9,13 +10,37 @@
}

export function login(returnTo = location.pathname + location.search) {
location.href = `/login?${new URLSearchParams({ origin: location.origin, returnTo })}`;
const params: Record<string, string> = {
origin: location.origin,
returnTo,
};

// forward any captured ad attribution to the dashboard's /login handler,
// which forwards it into auth0 authorizationParams so the post-login
// action can pass it to ensureUserForAuth.
const attribution = readAdAttribution();
if (attribution) {
if (attribution.li_fat_id) params.li_fat_id = attribution.li_fat_id;

Check failure on line 23 in src/ui/utils/auth.ts

View workflow job for this annotation

GitHub Actions / Trunk Check

eslint(curly)

[new] Expected { after 'if' condition.
if (attribution.twclid) params.twclid = attribution.twclid;

Check failure on line 24 in src/ui/utils/auth.ts

View workflow job for this annotation

GitHub Actions / Trunk Check

eslint(curly)

[new] Expected { after 'if' condition.
if (attribution.rdt_cid) params.rdt_cid = attribution.rdt_cid;

Check failure on line 25 in src/ui/utils/auth.ts

View workflow job for this annotation

GitHub Actions / Trunk Check

eslint(curly)

[new] Expected { after 'if' condition.
if (attribution.utm_source) params.utm_source = attribution.utm_source;

Check failure on line 26 in src/ui/utils/auth.ts

View workflow job for this annotation

GitHub Actions / Trunk Check

eslint(curly)

[new] Expected { after 'if' condition.
if (attribution.utm_medium) params.utm_medium = attribution.utm_medium;

Check failure on line 27 in src/ui/utils/auth.ts

View workflow job for this annotation

GitHub Actions / Trunk Check

eslint(curly)

[new] Expected { after 'if' condition.
if (attribution.utm_campaign) params.utm_campaign = attribution.utm_campaign;

Check failure on line 28 in src/ui/utils/auth.ts

View workflow job for this annotation

GitHub Actions / Trunk Check

eslint(curly)

[new] Expected { after 'if' condition.
if (attribution.utm_content) params.utm_content = attribution.utm_content;

Check failure on line 29 in src/ui/utils/auth.ts

View workflow job for this annotation

GitHub Actions / Trunk Check

eslint(curly)

[new] Expected { after 'if' condition.
if (attribution.utm_term) params.utm_term = attribution.utm_term;

Check failure on line 30 in src/ui/utils/auth.ts

View workflow job for this annotation

GitHub Actions / Trunk Check

eslint(curly)

[new] Expected { after 'if' condition.
}

location.href = `/login?${new URLSearchParams(params)}`;
}

export function logout() {
// clear the access token cookie
document.cookie = "replay:access-token=; expires=-1; Max-Age=-99999999; path=/;";

// drop any cached ad attribution so a later signup from a different
// user on the same browser doesn't inherit the previous user's click IDs.
clearAdAttribution();

if (window.__IS_RECORD_REPLAY_RUNTIME__) {
setAccessTokenInBrowserPrefs(null);
location.replace("/login");
Expand Down
Loading