forked from Mike/freno-dev
nook marketing - utm, analytics, etc
This commit is contained in:
82
src/server/utm.ts
Normal file
82
src/server/utm.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user