Skip to content

fix referrer encoding#44

Merged
dnywh merged 8 commits into
mainfrom
dnywh/fix-referrer-encoding
Apr 19, 2026
Merged

fix referrer encoding#44
dnywh merged 8 commits into
mainfrom
dnywh/fix-referrer-encoding

Conversation

@dnywh
Copy link
Copy Markdown
Owner

@dnywh dnywh commented Apr 18, 2026

Fixes the referrer encoding regression by storing the initial referrer cookie as a raw URL and normalising encoded values before signup.

Also handles already-stored encoded referrer values from existing browser sessions.

Check: npm run check

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
peels Ready Ready Preview, Comment Apr 19, 2026 1:54am

@supabase
Copy link
Copy Markdown

supabase Bot commented Apr 18, 2026

This pull request has been ignored for the connected project mfnaqdyunuafbwukbbyr because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes a referrer encoding regression in the attribution flow by switching initial-referrer storage to a “raw” URL and adding normalization for previously-encoded values across client storage and server-side signup.

Changes:

  • Added a referrer normalization helper that repeatedly decodes percent-encoded values (up to a limit) before persisting/using them.
  • Updated client-side attribution storage to normalize cookie/localStorage referrer values and repair already-stored encoded values.
  • Updated the proxy middleware to set the initial referrer cookie without manual encodeURIComponent.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.

File Description
src/utils/attributionUtils.ts Normalizes referrer values when reading from cookies/localStorage and when persisting the initial referrer.
src/proxy.ts Stops manually URL-encoding the initial referrer cookie value when setting it.
src/app/actions.ts Normalizes the submitted initial_referrer value before storing it in user metadata during signup.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/app/actions.ts Outdated
import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";

const normaliseReferrer = (referrer: string | undefined) => {
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

Naming: the repository uses normalize* in other utils; consider renaming normaliseReferrer to normalizeReferrer here as well for consistency and discoverability.

Copilot uses AI. Check for mistakes.
Comment thread src/app/actions.ts Outdated
Comment on lines +16 to +32
const normaliseReferrer = (referrer: string | undefined) => {
if (!referrer) return undefined;

let current = referrer;

for (let i = 0; i < 3; i += 1) {
try {
const decoded = decodeURIComponent(current);
if (decoded === current) break;
current = decoded;
} catch {
break;
}
}

return current;
};
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

This normaliseReferrer logic duplicates the helper added in src/utils/attributionUtils.ts. Consider extracting a shared helper and importing it, so the client and server normalisation rules don’t diverge over time.

Copilot uses AI. Check for mistakes.
Comment thread src/utils/attributionUtils.ts Outdated
Comment on lines 49 to 53
try {
return decodeURIComponent(value);
return normaliseReferrer(value);
} catch {
return value;
}
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

getCookie wraps normaliseReferrer(value) in a try/catch, but normaliseReferrer already catches decodeURIComponent errors internally and never throws. This try/catch is effectively dead code; consider removing it (or simplify normaliseReferrer to let errors bubble if you prefer handling them here).

Copilot uses AI. Check for mistakes.
Comment thread src/utils/attributionUtils.ts Outdated
Comment on lines +50 to +56
return normaliseReferrer(value);
} catch {
return value;
}
};

const normaliseReferrer = (referrer: string): string => {
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

Naming: the codebase already uses American spelling in similar helpers (e.g. normalizeNextPath, normalizeAssetPath). Consider renaming normaliseReferrer to normalizeReferrer for consistency and easier searchability.

Suggested change
return normaliseReferrer(value);
} catch {
return value;
}
};
const normaliseReferrer = (referrer: string): string => {
return normalizeReferrer(value);
} catch {
return value;
}
};
const normalizeReferrer = (referrer: string): string => {

Copilot uses AI. Check for mistakes.
Comment thread src/utils/attributionUtils.ts Outdated
Comment on lines +56 to +70
const normaliseReferrer = (referrer: string): string => {
let current = referrer;

for (let i = 0; i < 3; i += 1) {
try {
const decoded = decodeURIComponent(current);
if (decoded === current) break;
current = decoded;
} catch {
break;
}
}

return current;
};
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

normaliseReferrer is now implemented in both src/utils/attributionUtils.ts (client) and src/app/actions.ts (server) with the same logic. To avoid the two drifting over time, consider extracting this into a shared utility (e.g. src/utils/referrer.ts) and importing it from both places.

Copilot uses AI. Check for mistakes.
Comment thread src/proxy.ts
Comment on lines +39 to +45
response.cookies.set(INITIAL_REFERRER_COOKIE, externalReferrer, {
httpOnly: false,
maxAge: INITIAL_REFERRER_MAX_AGE,
path: "/",
sameSite: "lax",
secure: request.nextUrl.protocol === "https:",
});
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

Cookie values can’t safely contain certain separator characters (notably ;, ,, and whitespace). Since this now stores the raw externalReferrer string, a referrer path containing one of those characters could lead to a truncated/invalid cookie in browsers. Consider ensuring the value is cookie-safe here (e.g. rely on a cookie serializer that encodes, or explicitly encode/sanitize before calling cookies.set).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/utils/referrer.ts Outdated
Comment on lines +6 to +8
export function normalizeReferrer(referrer: string | undefined) {
if (!referrer) return undefined;

Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

normalizeReferrer treats an empty string as “no referrer” (if (!referrer) return undefined), but the overload normalizeReferrer(referrer: string): string promises a string return for all strings. Either change the check to only treat undefined as empty (e.g. referrer === undefined) or update the overloads/return type so callers can’t assume a string when passing an empty string.

Copilot uses AI. Check for mistakes.
Comment thread src/utils/referrer.ts Outdated
Comment on lines +11 to +18
for (let i = 0; i < 3; i += 1) {
try {
const decoded = decodeURIComponent(current);
if (decoded === current) break;
current = decoded;
} catch {
break;
}
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

The repeated decodeURIComponent loop can over-decode once the value is already a “raw URL”. For example, a previously double-encoded cookie can decode to a URL containing legitimate %2F/%3A sequences; the 3rd decode will turn those into //: and change the URL semantics. Consider only decoding while the string still looks like a fully-encoded URL wrapper (e.g. starts with http%3A/https%3A or contains %3A%2F%2F), and stop once you’ve reached an http(s):// form.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/utils/attributionUtils.ts Outdated
Comment on lines +126 to +133
const initialReferrer = storedInitialReferrer
? normaliseReferrer(storedInitialReferrer)
: cookieReferrer;

if (!storedInitialReferrer && initialReferrer) {
storeInitialReferrer(initialReferrer);
} else if (
storedInitialReferrer &&
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

storedInitialReferrer comes from localStorage.getItem(...) (type string | null), but the new conditional uses truthiness. This changes behavior vs the previous ?? logic: an empty string will now be treated as missing and fall back to the cookie. Use an explicit null check (e.g., storedInitialReferrer !== null) so empty-string values don’t change control flow unintentionally.

Suggested change
const initialReferrer = storedInitialReferrer
? normaliseReferrer(storedInitialReferrer)
: cookieReferrer;
if (!storedInitialReferrer && initialReferrer) {
storeInitialReferrer(initialReferrer);
} else if (
storedInitialReferrer &&
const initialReferrer =
storedInitialReferrer !== null
? normaliseReferrer(storedInitialReferrer)
: cookieReferrer;
if (storedInitialReferrer === null && initialReferrer) {
storeInitialReferrer(initialReferrer);
} else if (
storedInitialReferrer !== null &&

Copilot uses AI. Check for mistakes.
Comment thread src/app/actions.ts Outdated
Comment on lines +32 to +34
const referrer = normaliseReferrer(
formData.get("initial_referrer")?.toString()
);
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

normaliseReferrer is applied to formData.get("initial_referrer"), which is user-controllable. Because it can decode percent-encoded payloads, it can introduce control characters (e.g. %0d%0a) that then get logged and persisted to Supabase user metadata. Consider sanitizing the normalized value (strip control chars / enforce a max length) and/or validating it as an http(s) URL before storing/logging.

Copilot uses AI. Check for mistakes.
Comment thread src/utils/attributionUtils.ts
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/utils/referrer.ts
Comment on lines +5 to +10
export function normaliseReferrer(referrer: string): string;
export function normaliseReferrer(referrer: undefined): undefined;
export function normaliseReferrer(
referrer: string | undefined
): string | undefined;
export function normaliseReferrer(referrer: string | undefined) {
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

normaliseReferrer uses British spelling, but the rest of the codebase appears to consistently use normalize* (e.g. normalizeNextPath, normalizeAssetPath). Consider renaming to normalizeReferrer (and updating imports/usages) to keep naming consistent and improve discoverability.

Copilot uses AI. Check for mistakes.
Comment thread src/utils/attributionUtils.ts Outdated
} catch {
return value;
}
return normaliseReferrer(value) ?? null;
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

getCookie now returns normaliseReferrer(value) directly. Since normaliseReferrer can legally return an empty string (e.g. input contains only control characters), this function can return "" instead of null, which then blocks fallbacks that use nullish coalescing (??). Consider treating empty/whitespace-only results as null (e.g. trim + return null when empty) and drop the redundant ?? null (the overload for string returns string).

Suggested change
return normaliseReferrer(value) ?? null;
const normalisedValue = normaliseReferrer(value);
if (!normalisedValue.trim()) return null;
return normalisedValue;

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/utils/attributionUtils.ts Outdated
Comment on lines +52 to +55

if (!cleanedValue.trim()) return null;

return cleanedValue;
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

getCookie checks cleanedValue.trim() for emptiness but then returns the untrimmed cleanedValue. This can preserve leading/trailing whitespace in initial_referrer (and in localStorage via storeInitialReferrer), which can cause unnecessary mismatches and avoidable URL parse failures elsewhere. Consider returning the trimmed value (or trimming inside normaliseReferrer) so the stored/reflected referrer is consistently normalised.

Suggested change
if (!cleanedValue.trim()) return null;
return cleanedValue;
const trimmedValue = cleanedValue.trim();
if (!trimmedValue) return null;
return trimmedValue;

Copilot uses AI. Check for mistakes.
@dnywh dnywh merged commit 233e231 into main Apr 19, 2026
5 checks passed
@dnywh dnywh deleted the dnywh/fix-referrer-encoding branch April 19, 2026 02:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants