forked from Mike/freno-dev
83 lines
2.4 KiB
TypeScript
83 lines
2.4 KiB
TypeScript
/**
|
|
* Campaign parameters, parsed once and shared by every analytics writer.
|
|
*
|
|
* The three keys kept here are the ones a launch plan actually groups by:
|
|
* which channel (`utm_source`), through what (`utm_medium`), and which
|
|
* specific push (`utm_campaign`). Values are trimmed and length-capped,
|
|
* but never case-folded — an uppercase campaign name is a different
|
|
* campaign until the author says otherwise.
|
|
*
|
|
* `parseUtm` is variadic and first-non-empty-wins per key, which is what
|
|
* lets a logger pass both the URL it was called on and the referrer: a
|
|
* download click carries no query of its own, but the landing page it
|
|
* came from usually still does.
|
|
*/
|
|
|
|
export interface UtmParams {
|
|
utmSource: string | null;
|
|
utmMedium: string | null;
|
|
utmCampaign: string | null;
|
|
}
|
|
|
|
const EMPTY: UtmParams = {
|
|
utmSource: null,
|
|
utmMedium: null,
|
|
utmCampaign: null
|
|
};
|
|
|
|
/** Longest value we will store; far above any real campaign name. */
|
|
const MAX_LEN = 200;
|
|
|
|
/**
|
|
* Reads the query string out of a full URL, a path with a query, or a bare
|
|
* `a=1&b=2` fragment. Returns null when there is nothing to read.
|
|
*/
|
|
function searchOf(input: string): URLSearchParams | null {
|
|
try {
|
|
return new URL(input).searchParams;
|
|
} catch {
|
|
// Not absolute — take everything after the first "?".
|
|
const qIndex = input.indexOf("?");
|
|
if (qIndex === -1) return null;
|
|
return new URLSearchParams(input.slice(qIndex + 1));
|
|
}
|
|
}
|
|
|
|
function read(
|
|
search: URLSearchParams,
|
|
key: string
|
|
): string | null {
|
|
const value = search.get(key)?.trim();
|
|
if (!value) return null;
|
|
return value.slice(0, MAX_LEN);
|
|
}
|
|
|
|
/**
|
|
* Extracts UTM parameters from any number of URL-ish strings.
|
|
* Earlier arguments win per key; later ones only fill gaps.
|
|
*
|
|
* @example
|
|
* parseUtm("https://nook.freno.me/?utm_source=hn&utm_campaign=launch")
|
|
* // { utmSource: "hn", utmMedium: null, utmCampaign: "launch" }
|
|
*/
|
|
export function parseUtm(
|
|
...inputs: (string | null | undefined)[]
|
|
): UtmParams {
|
|
const result: UtmParams = { ...EMPTY };
|
|
|
|
for (const input of inputs) {
|
|
if (!input) continue;
|
|
// Everything present already — nothing left for later inputs to fill.
|
|
if (result.utmSource && result.utmMedium && result.utmCampaign) break;
|
|
|
|
const search = searchOf(input);
|
|
if (!search) continue;
|
|
|
|
result.utmSource ||= read(search, "utm_source");
|
|
result.utmMedium ||= read(search, "utm_medium");
|
|
result.utmCampaign ||= read(search, "utm_campaign");
|
|
}
|
|
|
|
return result;
|
|
}
|