@shware/analytics 7.1.1 → 7.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/click-id/index.cjs.map +1 -1
- package/dist/click-id/index.d.cts +1 -1
- package/dist/click-id/index.d.mts +1 -1
- package/dist/click-id/index.mjs.map +1 -1
- package/dist/constants/storage.cjs +2 -1
- package/dist/constants/storage.cjs.map +1 -1
- package/dist/constants/storage.d.cts +1 -0
- package/dist/constants/storage.d.cts.map +1 -1
- package/dist/constants/storage.d.mts +1 -0
- package/dist/constants/storage.d.mts.map +1 -1
- package/dist/constants/storage.mjs +2 -1
- package/dist/constants/storage.mjs.map +1 -1
- package/dist/hooks/use-app-analytics.cjs +1 -2
- package/dist/hooks/use-app-analytics.cjs.map +1 -1
- package/dist/hooks/use-app-analytics.mjs +1 -2
- package/dist/hooks/use-app-analytics.mjs.map +1 -1
- package/dist/hooks/use-outbound-click-analytics.cjs +1 -1
- package/dist/hooks/use-outbound-click-analytics.cjs.map +1 -1
- package/dist/hooks/use-outbound-click-analytics.mjs +1 -1
- package/dist/hooks/use-outbound-click-analytics.mjs.map +1 -1
- package/dist/hooks/use-web-analytics.cjs +1 -2
- package/dist/hooks/use-web-analytics.cjs.map +1 -1
- package/dist/hooks/use-web-analytics.mjs +1 -2
- package/dist/hooks/use-web-analytics.mjs.map +1 -1
- package/dist/next/index.cjs +1 -1
- package/dist/next/index.mjs +1 -1
- package/dist/react-router/index.cjs +1 -1
- package/dist/react-router/index.mjs +1 -1
- package/dist/schema/index.cjs +4 -2
- package/dist/schema/index.cjs.map +1 -1
- package/dist/schema/index.d.cts.map +1 -1
- package/dist/schema/index.d.mts.map +1 -1
- package/dist/schema/index.mjs +4 -2
- package/dist/schema/index.mjs.map +1 -1
- package/dist/setup/session.cjs +61 -14
- package/dist/setup/session.cjs.map +1 -1
- package/dist/setup/session.d.cts +24 -6
- package/dist/setup/session.d.cts.map +1 -1
- package/dist/setup/session.d.mts +24 -6
- package/dist/setup/session.d.mts.map +1 -1
- package/dist/setup/session.mjs +61 -14
- package/dist/setup/session.mjs.map +1 -1
- package/dist/tanstack/middleware.cjs.map +1 -1
- package/dist/tanstack/middleware.d.cts +1 -1
- package/dist/tanstack/middleware.d.mts +1 -1
- package/dist/tanstack/middleware.mjs.map +1 -1
- package/dist/track/index.cjs +17 -18
- package/dist/track/index.cjs.map +1 -1
- package/dist/track/index.d.cts.map +1 -1
- package/dist/track/index.d.mts.map +1 -1
- package/dist/track/index.mjs +17 -18
- package/dist/track/index.mjs.map +1 -1
- package/package.json +14 -14
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["cookie"],"sources":["../../src/click-id/index.ts"],"sourcesContent":["import { type SetCookie, parseCookie, stringifySetCookie } from 'cookie';\n\n/**\n * Server-side resolution of ad-click-id cookies (`_fbc`, `_rdt_cid`) from the incoming request.\n *\n * This is the framework-agnostic core meant to run on the *document* response (e.g. TanStack Start\n * server middleware, Next middleware). Setting `_fbc` via an HTTP `Set-Cookie` header on the top\n * document — rather than `document.cookie` on the client — is what Meta officially recommends and is\n * the only reliable way to keep the cookie alive for 90 days in Safari: ITP caps JavaScript-set\n * cookies on a fbclid-decorated landing page to 24 hours, and the document response is never\n * classified as CNAME/IP cloaking (it is the reference the browser measures cloaking against).\n *\n * reference: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc\n */\n\n// Meta's recommended _fbc cookie expiry. Events Manager warns (\"Server sending expired fbclid\")\n// when the embedded creationTime is older than 90 days and match quality/attribution may degrade;\n// no hard drop is documented.\nconst FBC_TTL_MS = 90 * 24 * 60 * 60 * 1000;\nconst RDT_CID_TTL_MS = 90 * 24 * 60 * 60 * 1000;\n// Tolerate a little clock skew when validating a creationTime against \"now\".\nconst CLOCK_SKEW_MS = 24 * 60 * 60 * 1000;\n\nexport const FBC_COOKIE = '_fbc';\nexport const RDT_CID_COOKIE = '_rdt_cid';\n\nexport type ParsedFbc = { raw: string; creationTime: number; fbclid: string };\n\n/**\n * Parse `fb.<subdomainIndex>.<creationTime>.<fbclid>`.\n * Returns undefined for anything malformed — a fbclid never contains a dot, but joining the tail\n * back together keeps us forward-compatible if that ever changes.\n */\nexport function parseFbc(\n raw: string | undefined | null,\n now: number = Date.now()\n): ParsedFbc | undefined {\n if (!raw) return undefined;\n const parts = raw.split('.');\n if (parts.length < 4 || parts[0] !== 'fb') return undefined;\n\n const creationTime = Number(parts[2]);\n const fbclid = parts.slice(3).join('.');\n if (!fbclid) return undefined;\n // creationTime is UNIX ms; reject seconds-precision or future-dated values as malformed.\n if (!Number.isFinite(creationTime)) return undefined;\n if (creationTime < 1e12 || creationTime > now + CLOCK_SKEW_MS) return undefined;\n\n return { raw, creationTime, fbclid };\n}\n\n/**\n * Build a fresh `_fbc` value. subdomainIndex is the cookie's domain level (com=0, example.com=1,\n * www.example.com=2); 1 is correct for an apex-hosted cookie.\n */\nexport function formatFbc(fbclid: string, now: number, subdomainIndex = 1): string {\n return `fb.${subdomainIndex}.${now}.${fbclid}`;\n}\n\nexport type ResolveClickIdCookiesInput = {\n /** The absolute request URL (must include the query string). */\n url: string;\n /** The raw `Cookie` request header, if any. */\n cookieHeader?: string | null;\n /** Overridable clock, primarily for tests. Defaults to `Date.now()`. */\n now?: number;\n /** `Domain` attribute for the emitted cookies, e.g. `.edensign.io`. Omit for a host-only cookie. */\n domain?: string;\n /** `Secure` attribute, default true. Set false only for local http testing. */\n secure?: boolean;\n /** subdomainIndex for a freshly built `_fbc` (see {@link formatFbc}). Default 1. */\n subdomainIndex?: number;\n /**\n * Re-issue a still-valid `_fbc` at its *remaining* lifetime on every call. On by default, as an\n * ITP self-heal: cookies follow last-writer-wins, so if the Meta Pixel overwrote `_fbc` via\n * `document.cookie` (Safari caps JS-written cookies on a fbclid-decorated landing to 24h),\n * re-issuing the long-lived HTTP cookie on the next navigation restores it — the same\n * continuous-re-issue mitigation the sGTM ecosystem uses (stape Cookie Keeper, Cookie Monster).\n * The value (and its creationTime) is byte-for-byte identical and the window never slides, so it\n * cannot trigger Meta's expired/modified-fbclid warnings.\n *\n * Set false to strictly follow Meta's documented conditional-write rule (\"only set the cookie if\n * it doesn't exist or the fbclid changed\") — e.g. to keep pages CDN-cacheable, since the\n * re-issue attaches a per-user `Set-Cookie` (forcing `no-store`) to every response carrying an\n * `_fbc`.\n */\n refresh?: boolean;\n};\n\nexport type ResolveClickIdCookiesResult = {\n /**\n * Cookies to emit on the response, ready for `Set-Cookie`. Empty when nothing needs to change.\n * A `value: ''`, `maxAge: 0` entry is a deletion (an expired or malformed leftover).\n */\n cookies: SetCookie[];\n /** The resolved `_fbc` value, for immediate server-side use (e.g. Conversions API). */\n fbc?: string;\n /** The resolved `_rdt_cid` value. */\n rdt_cid?: string;\n};\n\nfunction searchParams(url: string): URLSearchParams {\n try {\n return new URL(url).searchParams;\n } catch {\n // Fall back to a manual split so a relative or slightly malformed URL still yields the query.\n const q = url.indexOf('?');\n return new URLSearchParams(q !== -1 ? url.slice(q + 1) : '');\n }\n}\n\n/**\n * Resolve the click-id cookies to set on the current document response.\n *\n * `_fbc` follows Meta's documented conditional-write rule value-wise: a new fbclid (or an absent\n * cookie) opens a fresh 90-day window; a same-fbclid cookie keeps its original value and\n * `creationTime`. Expired (>90d) or malformed values are cleared instead of forwarded. By default,\n * ({@link ResolveClickIdCookiesInput.refresh}) a still-valid cookie is re-issued unchanged at its\n * *remaining* lifetime — never `now + 90d` — so a returning visitor's window cannot slide forward\n * (which is what makes Meta flag an expired fbclid).\n */\nexport function resolveClickIdCookies(\n input: ResolveClickIdCookiesInput\n): ResolveClickIdCookiesResult {\n const { url, cookieHeader, domain, secure = true, subdomainIndex = 1, refresh = true } = input;\n const now = input.now ?? Date.now();\n\n const params = searchParams(url);\n const jar = parseCookie(cookieHeader ?? '');\n const cookies: SetCookie[] = [];\n const result: ResolveClickIdCookiesResult = { cookies };\n\n const base = { path: '/', secure, sameSite: 'lax', domain } as const;\n const set = (name: string, value: string, ttlMs: number) =>\n cookies.push({ name, value, maxAge: Math.floor(ttlMs / 1000), ...base });\n const del = (name: string) => cookies.push({ name, value: '', maxAge: 0, ...base });\n\n // --- Meta _fbc ---\n const urlFbclid = params.get('fbclid') || undefined;\n const existingFbc = parseFbc(jar[FBC_COOKIE], now);\n\n if (urlFbclid && urlFbclid !== existingFbc?.fbclid) {\n // A new click always wins and opens a fresh 90-day window.\n const raw = formatFbc(urlFbclid, now, subdomainIndex);\n set(FBC_COOKIE, raw, FBC_TTL_MS);\n result.fbc = raw;\n } else if (existingFbc) {\n const remainingMs = existingFbc.creationTime + FBC_TTL_MS - now;\n if (remainingMs <= 0) {\n del(FBC_COOKIE);\n } else {\n // Same fbclid: the value and creationTime are preserved (Meta's rule); refresh (default)\n // re-issues it unchanged at its remaining lifetime as an ITP self-heal.\n result.fbc = existingFbc.raw;\n if (refresh) set(FBC_COOKIE, existingFbc.raw, remainingMs);\n }\n } else if (jar[FBC_COOKIE]) {\n // Malformed leftover — clear it rather than forwarding it to Meta.\n del(FBC_COOKIE);\n }\n\n // --- Reddit _rdt_cid ---\n // No embedded timestamp, so it can only be anchored at first capture; set it once from the URL and\n // otherwise leave the existing cookie untouched (re-issuing would slide its window).\n const urlRdtCid = params.get('rdt_cid') || undefined;\n const existingRdtCid = jar[RDT_CID_COOKIE] || undefined;\n if (urlRdtCid && urlRdtCid !== existingRdtCid) {\n set(RDT_CID_COOKIE, urlRdtCid, RDT_CID_TTL_MS);\n result.rdt_cid = urlRdtCid;\n } else if (existingRdtCid) {\n result.rdt_cid = existingRdtCid;\n }\n\n return result;\n}\n\n/** Serialize the resolved cookies into `Set-Cookie` header values. */\nexport function toSetCookieHeaders(cookies: SetCookie[]): string[] {\n return cookies.map((cookie) => stringifySetCookie(cookie));\n}\n"],"mappings":";;;;;;;;;;;;;;;AAkBA,MAAM,aAAa,OAAU,KAAK,KAAK;AACvC,MAAM,iBAAiB,OAAU,KAAK,KAAK;AAE3C,MAAM,gBAAgB,OAAU,KAAK;AAErC,MAAa,aAAa;AAC1B,MAAa,iBAAiB;;;;;;AAS9B,SAAgB,SACd,KACA,MAAc,KAAK,IAAI,GACA;CACvB,IAAI,CAAC,KAAK,OAAO,KAAA;CACjB,MAAM,QAAQ,IAAI,MAAM,GAAG;CAC3B,IAAI,MAAM,SAAS,KAAK,MAAM,OAAO,MAAM,OAAO,KAAA;CAElD,MAAM,eAAe,OAAO,MAAM,EAAE;CACpC,MAAM,SAAS,MAAM,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG;CACtC,IAAI,CAAC,QAAQ,OAAO,KAAA;CAEpB,IAAI,CAAC,OAAO,SAAS,YAAY,GAAG,OAAO,KAAA;CAC3C,IAAI,eAAe,gBAAQ,eAAe,MAAM,eAAe,OAAO,KAAA;CAEtE,OAAO;EAAE;EAAK;EAAc;CAAO;AACrC;;;;;AAMA,SAAgB,UAAU,QAAgB,KAAa,iBAAiB,GAAW;CACjF,OAAO,MAAM,eAAe,GAAG,IAAI,GAAG;AACxC;AA4CA,SAAS,aAAa,KAA8B;CAClD,IAAI;EACF,OAAO,IAAI,IAAI,GAAG,CAAC,CAAC;CACtB,QAAQ;EAEN,MAAM,IAAI,IAAI,QAAQ,GAAG;EACzB,OAAO,IAAI,gBAAgB,MAAM,KAAK,IAAI,MAAM,IAAI,CAAC,IAAI,EAAE;CAC7D;AACF;;;;;;;;;;;AAYA,SAAgB,sBACd,OAC6B;CAC7B,MAAM,EAAE,KAAK,cAAc,QAAQ,SAAS,MAAM,iBAAiB,GAAG,UAAU,SAAS;CACzF,MAAM,MAAM,MAAM,OAAO,KAAK,IAAI;CAElC,MAAM,SAAS,aAAa,GAAG;CAC/B,MAAM,OAAA,GAAA,OAAA,YAAA,CAAkB,gBAAgB,EAAE;CAC1C,MAAM,UAAuB,CAAC;CAC9B,MAAM,SAAsC,EAAE,QAAQ;CAEtD,MAAM,OAAO;EAAE,MAAM;EAAK;EAAQ,UAAU;EAAO;CAAO;CAC1D,MAAM,OAAO,MAAc,OAAe,UACxC,QAAQ,KAAK;EAAE;EAAM;EAAO,QAAQ,KAAK,MAAM,QAAQ,GAAI;EAAG,GAAG;CAAK,CAAC;CACzE,MAAM,OAAO,SAAiB,QAAQ,KAAK;EAAE;EAAM,OAAO;EAAI,QAAQ;EAAG,GAAG;CAAK,CAAC;CAGlF,MAAM,YAAY,OAAO,IAAI,QAAQ,KAAK,KAAA;CAC1C,MAAM,cAAc,SAAS,IAAI,aAAa,GAAG;CAEjD,IAAI,aAAa,cAAc,aAAa,QAAQ;EAElD,MAAM,MAAM,UAAU,WAAW,KAAK,cAAc;EACpD,IAAI,YAAY,KAAK,UAAU;EAC/B,OAAO,MAAM;CACf,OAAO,IAAI,aAAa;EACtB,MAAM,cAAc,YAAY,eAAe,aAAa;EAC5D,IAAI,eAAe,GACjB,IAAI,UAAU;OACT;GAGL,OAAO,MAAM,YAAY;GACzB,IAAI,SAAS,IAAI,YAAY,YAAY,KAAK,WAAW;EAC3D;CACF,OAAO,IAAI,IAAA,SAET,IAAI,UAAU;CAMhB,MAAM,YAAY,OAAO,IAAI,SAAS,KAAK,KAAA;CAC3C,MAAM,iBAAiB,IAAA,eAAuB,KAAA;CAC9C,IAAI,aAAa,cAAc,gBAAgB;EAC7C,IAAI,gBAAgB,WAAW,cAAc;EAC7C,OAAO,UAAU;CACnB,OAAO,IAAI,gBACT,OAAO,UAAU;CAGnB,OAAO;AACT;;AAGA,SAAgB,mBAAmB,SAAgC;CACjE,OAAO,QAAQ,KAAK,cAAA,GAAA,OAAA,mBAAA,CAA8BA,QAAM,CAAC;AAC3D"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["cookie"],"sources":["../../src/click-id/index.ts"],"sourcesContent":["import { type SetCookie, parseCookie, stringifySetCookie } from 'cookie';\n\n/**\n * Server-side resolution of ad-click-id cookies (`_fbc`, `_rdt_cid`) from the incoming request.\n *\n * This is the framework-agnostic core meant to run on the *document* response (e.g. TanStack Start\n * server middleware, Next middleware). Setting `_fbc` via an HTTP `Set-Cookie` header on the top\n * document — rather than `document.cookie` on the client — is what Meta officially recommends and is\n * the only reliable way to keep the cookie alive for 90 days in Safari: ITP caps JavaScript-set\n * cookies on a fbclid-decorated landing page to 24 hours, and the document response is never\n * classified as CNAME/IP cloaking (it is the reference the browser measures cloaking against).\n *\n * reference: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc\n */\n\n// Meta's recommended _fbc cookie expiry. Events Manager warns (\"Server sending expired fbclid\")\n// when the embedded creationTime is older than 90 days and match quality/attribution may degrade;\n// no hard drop is documented.\nconst FBC_TTL_MS = 90 * 24 * 60 * 60 * 1000;\nconst RDT_CID_TTL_MS = 90 * 24 * 60 * 60 * 1000;\n// Tolerate a little clock skew when validating a creationTime against \"now\".\nconst CLOCK_SKEW_MS = 24 * 60 * 60 * 1000;\n\nexport const FBC_COOKIE = '_fbc';\nexport const RDT_CID_COOKIE = '_rdt_cid';\n\nexport type ParsedFbc = { raw: string; creationTime: number; fbclid: string };\n\n/**\n * Parse `fb.<subdomainIndex>.<creationTime>.<fbclid>`.\n * Returns undefined for anything malformed — a fbclid never contains a dot, but joining the tail\n * back together keeps us forward-compatible if that ever changes.\n */\nexport function parseFbc(\n raw: string | undefined | null,\n now: number = Date.now()\n): ParsedFbc | undefined {\n if (!raw) return undefined;\n const parts = raw.split('.');\n if (parts.length < 4 || parts[0] !== 'fb') return undefined;\n\n const creationTime = Number(parts[2]);\n const fbclid = parts.slice(3).join('.');\n if (!fbclid) return undefined;\n // creationTime is UNIX ms; reject seconds-precision or future-dated values as malformed.\n if (!Number.isFinite(creationTime)) return undefined;\n if (creationTime < 1e12 || creationTime > now + CLOCK_SKEW_MS) return undefined;\n\n return { raw, creationTime, fbclid };\n}\n\n/**\n * Build a fresh `_fbc` value. subdomainIndex is the cookie's domain level (com=0, example.com=1,\n * www.example.com=2); 1 is correct for an apex-hosted cookie.\n */\nexport function formatFbc(fbclid: string, now: number, subdomainIndex = 1): string {\n return `fb.${subdomainIndex}.${now}.${fbclid}`;\n}\n\nexport type ResolveClickIdCookiesInput = {\n /** The absolute request URL (must include the query string). */\n url: string;\n /** The raw `Cookie` request header, if any. */\n cookieHeader?: string | null;\n /** Overridable clock, primarily for tests. Defaults to `Date.now()`. */\n now?: number;\n /** `Domain` attribute for the emitted cookies, e.g. `.shware.io`. Omit for a host-only cookie. */\n domain?: string;\n /** `Secure` attribute, default true. Set false only for local http testing. */\n secure?: boolean;\n /** subdomainIndex for a freshly built `_fbc` (see {@link formatFbc}). Default 1. */\n subdomainIndex?: number;\n /**\n * Re-issue a still-valid `_fbc` at its *remaining* lifetime on every call. On by default, as an\n * ITP self-heal: cookies follow last-writer-wins, so if the Meta Pixel overwrote `_fbc` via\n * `document.cookie` (Safari caps JS-written cookies on a fbclid-decorated landing to 24h),\n * re-issuing the long-lived HTTP cookie on the next navigation restores it — the same\n * continuous-re-issue mitigation the sGTM ecosystem uses (stape Cookie Keeper, Cookie Monster).\n * The value (and its creationTime) is byte-for-byte identical and the window never slides, so it\n * cannot trigger Meta's expired/modified-fbclid warnings.\n *\n * Set false to strictly follow Meta's documented conditional-write rule (\"only set the cookie if\n * it doesn't exist or the fbclid changed\") — e.g. to keep pages CDN-cacheable, since the\n * re-issue attaches a per-user `Set-Cookie` (forcing `no-store`) to every response carrying an\n * `_fbc`.\n */\n refresh?: boolean;\n};\n\nexport type ResolveClickIdCookiesResult = {\n /**\n * Cookies to emit on the response, ready for `Set-Cookie`. Empty when nothing needs to change.\n * A `value: ''`, `maxAge: 0` entry is a deletion (an expired or malformed leftover).\n */\n cookies: SetCookie[];\n /** The resolved `_fbc` value, for immediate server-side use (e.g. Conversions API). */\n fbc?: string;\n /** The resolved `_rdt_cid` value. */\n rdt_cid?: string;\n};\n\nfunction searchParams(url: string): URLSearchParams {\n try {\n return new URL(url).searchParams;\n } catch {\n // Fall back to a manual split so a relative or slightly malformed URL still yields the query.\n const q = url.indexOf('?');\n return new URLSearchParams(q !== -1 ? url.slice(q + 1) : '');\n }\n}\n\n/**\n * Resolve the click-id cookies to set on the current document response.\n *\n * `_fbc` follows Meta's documented conditional-write rule value-wise: a new fbclid (or an absent\n * cookie) opens a fresh 90-day window; a same-fbclid cookie keeps its original value and\n * `creationTime`. Expired (>90d) or malformed values are cleared instead of forwarded. By default,\n * ({@link ResolveClickIdCookiesInput.refresh}) a still-valid cookie is re-issued unchanged at its\n * *remaining* lifetime — never `now + 90d` — so a returning visitor's window cannot slide forward\n * (which is what makes Meta flag an expired fbclid).\n */\nexport function resolveClickIdCookies(\n input: ResolveClickIdCookiesInput\n): ResolveClickIdCookiesResult {\n const { url, cookieHeader, domain, secure = true, subdomainIndex = 1, refresh = true } = input;\n const now = input.now ?? Date.now();\n\n const params = searchParams(url);\n const jar = parseCookie(cookieHeader ?? '');\n const cookies: SetCookie[] = [];\n const result: ResolveClickIdCookiesResult = { cookies };\n\n const base = { path: '/', secure, sameSite: 'lax', domain } as const;\n const set = (name: string, value: string, ttlMs: number) =>\n cookies.push({ name, value, maxAge: Math.floor(ttlMs / 1000), ...base });\n const del = (name: string) => cookies.push({ name, value: '', maxAge: 0, ...base });\n\n // --- Meta _fbc ---\n const urlFbclid = params.get('fbclid') || undefined;\n const existingFbc = parseFbc(jar[FBC_COOKIE], now);\n\n if (urlFbclid && urlFbclid !== existingFbc?.fbclid) {\n // A new click always wins and opens a fresh 90-day window.\n const raw = formatFbc(urlFbclid, now, subdomainIndex);\n set(FBC_COOKIE, raw, FBC_TTL_MS);\n result.fbc = raw;\n } else if (existingFbc) {\n const remainingMs = existingFbc.creationTime + FBC_TTL_MS - now;\n if (remainingMs <= 0) {\n del(FBC_COOKIE);\n } else {\n // Same fbclid: the value and creationTime are preserved (Meta's rule); refresh (default)\n // re-issues it unchanged at its remaining lifetime as an ITP self-heal.\n result.fbc = existingFbc.raw;\n if (refresh) set(FBC_COOKIE, existingFbc.raw, remainingMs);\n }\n } else if (jar[FBC_COOKIE]) {\n // Malformed leftover — clear it rather than forwarding it to Meta.\n del(FBC_COOKIE);\n }\n\n // --- Reddit _rdt_cid ---\n // No embedded timestamp, so it can only be anchored at first capture; set it once from the URL and\n // otherwise leave the existing cookie untouched (re-issuing would slide its window).\n const urlRdtCid = params.get('rdt_cid') || undefined;\n const existingRdtCid = jar[RDT_CID_COOKIE] || undefined;\n if (urlRdtCid && urlRdtCid !== existingRdtCid) {\n set(RDT_CID_COOKIE, urlRdtCid, RDT_CID_TTL_MS);\n result.rdt_cid = urlRdtCid;\n } else if (existingRdtCid) {\n result.rdt_cid = existingRdtCid;\n }\n\n return result;\n}\n\n/** Serialize the resolved cookies into `Set-Cookie` header values. */\nexport function toSetCookieHeaders(cookies: SetCookie[]): string[] {\n return cookies.map((cookie) => stringifySetCookie(cookie));\n}\n"],"mappings":";;;;;;;;;;;;;;;AAkBA,MAAM,aAAa,OAAU,KAAK,KAAK;AACvC,MAAM,iBAAiB,OAAU,KAAK,KAAK;AAE3C,MAAM,gBAAgB,OAAU,KAAK;AAErC,MAAa,aAAa;AAC1B,MAAa,iBAAiB;;;;;;AAS9B,SAAgB,SACd,KACA,MAAc,KAAK,IAAI,GACA;CACvB,IAAI,CAAC,KAAK,OAAO,KAAA;CACjB,MAAM,QAAQ,IAAI,MAAM,GAAG;CAC3B,IAAI,MAAM,SAAS,KAAK,MAAM,OAAO,MAAM,OAAO,KAAA;CAElD,MAAM,eAAe,OAAO,MAAM,EAAE;CACpC,MAAM,SAAS,MAAM,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG;CACtC,IAAI,CAAC,QAAQ,OAAO,KAAA;CAEpB,IAAI,CAAC,OAAO,SAAS,YAAY,GAAG,OAAO,KAAA;CAC3C,IAAI,eAAe,gBAAQ,eAAe,MAAM,eAAe,OAAO,KAAA;CAEtE,OAAO;EAAE;EAAK;EAAc;CAAO;AACrC;;;;;AAMA,SAAgB,UAAU,QAAgB,KAAa,iBAAiB,GAAW;CACjF,OAAO,MAAM,eAAe,GAAG,IAAI,GAAG;AACxC;AA4CA,SAAS,aAAa,KAA8B;CAClD,IAAI;EACF,OAAO,IAAI,IAAI,GAAG,CAAC,CAAC;CACtB,QAAQ;EAEN,MAAM,IAAI,IAAI,QAAQ,GAAG;EACzB,OAAO,IAAI,gBAAgB,MAAM,KAAK,IAAI,MAAM,IAAI,CAAC,IAAI,EAAE;CAC7D;AACF;;;;;;;;;;;AAYA,SAAgB,sBACd,OAC6B;CAC7B,MAAM,EAAE,KAAK,cAAc,QAAQ,SAAS,MAAM,iBAAiB,GAAG,UAAU,SAAS;CACzF,MAAM,MAAM,MAAM,OAAO,KAAK,IAAI;CAElC,MAAM,SAAS,aAAa,GAAG;CAC/B,MAAM,OAAA,GAAA,OAAA,YAAA,CAAkB,gBAAgB,EAAE;CAC1C,MAAM,UAAuB,CAAC;CAC9B,MAAM,SAAsC,EAAE,QAAQ;CAEtD,MAAM,OAAO;EAAE,MAAM;EAAK;EAAQ,UAAU;EAAO;CAAO;CAC1D,MAAM,OAAO,MAAc,OAAe,UACxC,QAAQ,KAAK;EAAE;EAAM;EAAO,QAAQ,KAAK,MAAM,QAAQ,GAAI;EAAG,GAAG;CAAK,CAAC;CACzE,MAAM,OAAO,SAAiB,QAAQ,KAAK;EAAE;EAAM,OAAO;EAAI,QAAQ;EAAG,GAAG;CAAK,CAAC;CAGlF,MAAM,YAAY,OAAO,IAAI,QAAQ,KAAK,KAAA;CAC1C,MAAM,cAAc,SAAS,IAAI,aAAa,GAAG;CAEjD,IAAI,aAAa,cAAc,aAAa,QAAQ;EAElD,MAAM,MAAM,UAAU,WAAW,KAAK,cAAc;EACpD,IAAI,YAAY,KAAK,UAAU;EAC/B,OAAO,MAAM;CACf,OAAO,IAAI,aAAa;EACtB,MAAM,cAAc,YAAY,eAAe,aAAa;EAC5D,IAAI,eAAe,GACjB,IAAI,UAAU;OACT;GAGL,OAAO,MAAM,YAAY;GACzB,IAAI,SAAS,IAAI,YAAY,YAAY,KAAK,WAAW;EAC3D;CACF,OAAO,IAAI,IAAA,SAET,IAAI,UAAU;CAMhB,MAAM,YAAY,OAAO,IAAI,SAAS,KAAK,KAAA;CAC3C,MAAM,iBAAiB,IAAA,eAAuB,KAAA;CAC9C,IAAI,aAAa,cAAc,gBAAgB;EAC7C,IAAI,gBAAgB,WAAW,cAAc;EAC7C,OAAO,UAAU;CACnB,OAAO,IAAI,gBACT,OAAO,UAAU;CAGnB,OAAO;AACT;;AAGA,SAAgB,mBAAmB,SAAgC;CACjE,OAAO,QAAQ,KAAK,cAAA,GAAA,OAAA,mBAAA,CAA8BA,QAAM,CAAC;AAC3D"}
|
|
@@ -25,7 +25,7 @@ type ResolveClickIdCookiesInput = {
|
|
|
25
25
|
cookieHeader?: string | null;
|
|
26
26
|
/** Overridable clock, primarily for tests. Defaults to `Date.now()`. */
|
|
27
27
|
now?: number;
|
|
28
|
-
/** `Domain` attribute for the emitted cookies, e.g. `.
|
|
28
|
+
/** `Domain` attribute for the emitted cookies, e.g. `.shware.io`. Omit for a host-only cookie. */
|
|
29
29
|
domain?: string;
|
|
30
30
|
/** `Secure` attribute, default true. Set false only for local http testing. */
|
|
31
31
|
secure?: boolean;
|
|
@@ -25,7 +25,7 @@ type ResolveClickIdCookiesInput = {
|
|
|
25
25
|
cookieHeader?: string | null;
|
|
26
26
|
/** Overridable clock, primarily for tests. Defaults to `Date.now()`. */
|
|
27
27
|
now?: number;
|
|
28
|
-
/** `Domain` attribute for the emitted cookies, e.g. `.
|
|
28
|
+
/** `Domain` attribute for the emitted cookies, e.g. `.shware.io`. Omit for a host-only cookie. */
|
|
29
29
|
domain?: string;
|
|
30
30
|
/** `Secure` attribute, default true. Set false only for local http testing. */
|
|
31
31
|
secure?: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/click-id/index.ts"],"sourcesContent":["import { type SetCookie, parseCookie, stringifySetCookie } from 'cookie';\n\n/**\n * Server-side resolution of ad-click-id cookies (`_fbc`, `_rdt_cid`) from the incoming request.\n *\n * This is the framework-agnostic core meant to run on the *document* response (e.g. TanStack Start\n * server middleware, Next middleware). Setting `_fbc` via an HTTP `Set-Cookie` header on the top\n * document — rather than `document.cookie` on the client — is what Meta officially recommends and is\n * the only reliable way to keep the cookie alive for 90 days in Safari: ITP caps JavaScript-set\n * cookies on a fbclid-decorated landing page to 24 hours, and the document response is never\n * classified as CNAME/IP cloaking (it is the reference the browser measures cloaking against).\n *\n * reference: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc\n */\n\n// Meta's recommended _fbc cookie expiry. Events Manager warns (\"Server sending expired fbclid\")\n// when the embedded creationTime is older than 90 days and match quality/attribution may degrade;\n// no hard drop is documented.\nconst FBC_TTL_MS = 90 * 24 * 60 * 60 * 1000;\nconst RDT_CID_TTL_MS = 90 * 24 * 60 * 60 * 1000;\n// Tolerate a little clock skew when validating a creationTime against \"now\".\nconst CLOCK_SKEW_MS = 24 * 60 * 60 * 1000;\n\nexport const FBC_COOKIE = '_fbc';\nexport const RDT_CID_COOKIE = '_rdt_cid';\n\nexport type ParsedFbc = { raw: string; creationTime: number; fbclid: string };\n\n/**\n * Parse `fb.<subdomainIndex>.<creationTime>.<fbclid>`.\n * Returns undefined for anything malformed — a fbclid never contains a dot, but joining the tail\n * back together keeps us forward-compatible if that ever changes.\n */\nexport function parseFbc(\n raw: string | undefined | null,\n now: number = Date.now()\n): ParsedFbc | undefined {\n if (!raw) return undefined;\n const parts = raw.split('.');\n if (parts.length < 4 || parts[0] !== 'fb') return undefined;\n\n const creationTime = Number(parts[2]);\n const fbclid = parts.slice(3).join('.');\n if (!fbclid) return undefined;\n // creationTime is UNIX ms; reject seconds-precision or future-dated values as malformed.\n if (!Number.isFinite(creationTime)) return undefined;\n if (creationTime < 1e12 || creationTime > now + CLOCK_SKEW_MS) return undefined;\n\n return { raw, creationTime, fbclid };\n}\n\n/**\n * Build a fresh `_fbc` value. subdomainIndex is the cookie's domain level (com=0, example.com=1,\n * www.example.com=2); 1 is correct for an apex-hosted cookie.\n */\nexport function formatFbc(fbclid: string, now: number, subdomainIndex = 1): string {\n return `fb.${subdomainIndex}.${now}.${fbclid}`;\n}\n\nexport type ResolveClickIdCookiesInput = {\n /** The absolute request URL (must include the query string). */\n url: string;\n /** The raw `Cookie` request header, if any. */\n cookieHeader?: string | null;\n /** Overridable clock, primarily for tests. Defaults to `Date.now()`. */\n now?: number;\n /** `Domain` attribute for the emitted cookies, e.g. `.edensign.io`. Omit for a host-only cookie. */\n domain?: string;\n /** `Secure` attribute, default true. Set false only for local http testing. */\n secure?: boolean;\n /** subdomainIndex for a freshly built `_fbc` (see {@link formatFbc}). Default 1. */\n subdomainIndex?: number;\n /**\n * Re-issue a still-valid `_fbc` at its *remaining* lifetime on every call. On by default, as an\n * ITP self-heal: cookies follow last-writer-wins, so if the Meta Pixel overwrote `_fbc` via\n * `document.cookie` (Safari caps JS-written cookies on a fbclid-decorated landing to 24h),\n * re-issuing the long-lived HTTP cookie on the next navigation restores it — the same\n * continuous-re-issue mitigation the sGTM ecosystem uses (stape Cookie Keeper, Cookie Monster).\n * The value (and its creationTime) is byte-for-byte identical and the window never slides, so it\n * cannot trigger Meta's expired/modified-fbclid warnings.\n *\n * Set false to strictly follow Meta's documented conditional-write rule (\"only set the cookie if\n * it doesn't exist or the fbclid changed\") — e.g. to keep pages CDN-cacheable, since the\n * re-issue attaches a per-user `Set-Cookie` (forcing `no-store`) to every response carrying an\n * `_fbc`.\n */\n refresh?: boolean;\n};\n\nexport type ResolveClickIdCookiesResult = {\n /**\n * Cookies to emit on the response, ready for `Set-Cookie`. Empty when nothing needs to change.\n * A `value: ''`, `maxAge: 0` entry is a deletion (an expired or malformed leftover).\n */\n cookies: SetCookie[];\n /** The resolved `_fbc` value, for immediate server-side use (e.g. Conversions API). */\n fbc?: string;\n /** The resolved `_rdt_cid` value. */\n rdt_cid?: string;\n};\n\nfunction searchParams(url: string): URLSearchParams {\n try {\n return new URL(url).searchParams;\n } catch {\n // Fall back to a manual split so a relative or slightly malformed URL still yields the query.\n const q = url.indexOf('?');\n return new URLSearchParams(q !== -1 ? url.slice(q + 1) : '');\n }\n}\n\n/**\n * Resolve the click-id cookies to set on the current document response.\n *\n * `_fbc` follows Meta's documented conditional-write rule value-wise: a new fbclid (or an absent\n * cookie) opens a fresh 90-day window; a same-fbclid cookie keeps its original value and\n * `creationTime`. Expired (>90d) or malformed values are cleared instead of forwarded. By default,\n * ({@link ResolveClickIdCookiesInput.refresh}) a still-valid cookie is re-issued unchanged at its\n * *remaining* lifetime — never `now + 90d` — so a returning visitor's window cannot slide forward\n * (which is what makes Meta flag an expired fbclid).\n */\nexport function resolveClickIdCookies(\n input: ResolveClickIdCookiesInput\n): ResolveClickIdCookiesResult {\n const { url, cookieHeader, domain, secure = true, subdomainIndex = 1, refresh = true } = input;\n const now = input.now ?? Date.now();\n\n const params = searchParams(url);\n const jar = parseCookie(cookieHeader ?? '');\n const cookies: SetCookie[] = [];\n const result: ResolveClickIdCookiesResult = { cookies };\n\n const base = { path: '/', secure, sameSite: 'lax', domain } as const;\n const set = (name: string, value: string, ttlMs: number) =>\n cookies.push({ name, value, maxAge: Math.floor(ttlMs / 1000), ...base });\n const del = (name: string) => cookies.push({ name, value: '', maxAge: 0, ...base });\n\n // --- Meta _fbc ---\n const urlFbclid = params.get('fbclid') || undefined;\n const existingFbc = parseFbc(jar[FBC_COOKIE], now);\n\n if (urlFbclid && urlFbclid !== existingFbc?.fbclid) {\n // A new click always wins and opens a fresh 90-day window.\n const raw = formatFbc(urlFbclid, now, subdomainIndex);\n set(FBC_COOKIE, raw, FBC_TTL_MS);\n result.fbc = raw;\n } else if (existingFbc) {\n const remainingMs = existingFbc.creationTime + FBC_TTL_MS - now;\n if (remainingMs <= 0) {\n del(FBC_COOKIE);\n } else {\n // Same fbclid: the value and creationTime are preserved (Meta's rule); refresh (default)\n // re-issues it unchanged at its remaining lifetime as an ITP self-heal.\n result.fbc = existingFbc.raw;\n if (refresh) set(FBC_COOKIE, existingFbc.raw, remainingMs);\n }\n } else if (jar[FBC_COOKIE]) {\n // Malformed leftover — clear it rather than forwarding it to Meta.\n del(FBC_COOKIE);\n }\n\n // --- Reddit _rdt_cid ---\n // No embedded timestamp, so it can only be anchored at first capture; set it once from the URL and\n // otherwise leave the existing cookie untouched (re-issuing would slide its window).\n const urlRdtCid = params.get('rdt_cid') || undefined;\n const existingRdtCid = jar[RDT_CID_COOKIE] || undefined;\n if (urlRdtCid && urlRdtCid !== existingRdtCid) {\n set(RDT_CID_COOKIE, urlRdtCid, RDT_CID_TTL_MS);\n result.rdt_cid = urlRdtCid;\n } else if (existingRdtCid) {\n result.rdt_cid = existingRdtCid;\n }\n\n return result;\n}\n\n/** Serialize the resolved cookies into `Set-Cookie` header values. */\nexport function toSetCookieHeaders(cookies: SetCookie[]): string[] {\n return cookies.map((cookie) => stringifySetCookie(cookie));\n}\n"],"mappings":";;;;;;;;;;;;;;AAkBA,MAAM,aAAa,OAAU,KAAK,KAAK;AACvC,MAAM,iBAAiB,OAAU,KAAK,KAAK;AAE3C,MAAM,gBAAgB,OAAU,KAAK;AAErC,MAAa,aAAa;AAC1B,MAAa,iBAAiB;;;;;;AAS9B,SAAgB,SACd,KACA,MAAc,KAAK,IAAI,GACA;CACvB,IAAI,CAAC,KAAK,OAAO,KAAA;CACjB,MAAM,QAAQ,IAAI,MAAM,GAAG;CAC3B,IAAI,MAAM,SAAS,KAAK,MAAM,OAAO,MAAM,OAAO,KAAA;CAElD,MAAM,eAAe,OAAO,MAAM,EAAE;CACpC,MAAM,SAAS,MAAM,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG;CACtC,IAAI,CAAC,QAAQ,OAAO,KAAA;CAEpB,IAAI,CAAC,OAAO,SAAS,YAAY,GAAG,OAAO,KAAA;CAC3C,IAAI,eAAe,gBAAQ,eAAe,MAAM,eAAe,OAAO,KAAA;CAEtE,OAAO;EAAE;EAAK;EAAc;CAAO;AACrC;;;;;AAMA,SAAgB,UAAU,QAAgB,KAAa,iBAAiB,GAAW;CACjF,OAAO,MAAM,eAAe,GAAG,IAAI,GAAG;AACxC;AA4CA,SAAS,aAAa,KAA8B;CAClD,IAAI;EACF,OAAO,IAAI,IAAI,GAAG,CAAC,CAAC;CACtB,QAAQ;EAEN,MAAM,IAAI,IAAI,QAAQ,GAAG;EACzB,OAAO,IAAI,gBAAgB,MAAM,KAAK,IAAI,MAAM,IAAI,CAAC,IAAI,EAAE;CAC7D;AACF;;;;;;;;;;;AAYA,SAAgB,sBACd,OAC6B;CAC7B,MAAM,EAAE,KAAK,cAAc,QAAQ,SAAS,MAAM,iBAAiB,GAAG,UAAU,SAAS;CACzF,MAAM,MAAM,MAAM,OAAO,KAAK,IAAI;CAElC,MAAM,SAAS,aAAa,GAAG;CAC/B,MAAM,MAAM,YAAY,gBAAgB,EAAE;CAC1C,MAAM,UAAuB,CAAC;CAC9B,MAAM,SAAsC,EAAE,QAAQ;CAEtD,MAAM,OAAO;EAAE,MAAM;EAAK;EAAQ,UAAU;EAAO;CAAO;CAC1D,MAAM,OAAO,MAAc,OAAe,UACxC,QAAQ,KAAK;EAAE;EAAM;EAAO,QAAQ,KAAK,MAAM,QAAQ,GAAI;EAAG,GAAG;CAAK,CAAC;CACzE,MAAM,OAAO,SAAiB,QAAQ,KAAK;EAAE;EAAM,OAAO;EAAI,QAAQ;EAAG,GAAG;CAAK,CAAC;CAGlF,MAAM,YAAY,OAAO,IAAI,QAAQ,KAAK,KAAA;CAC1C,MAAM,cAAc,SAAS,IAAI,aAAa,GAAG;CAEjD,IAAI,aAAa,cAAc,aAAa,QAAQ;EAElD,MAAM,MAAM,UAAU,WAAW,KAAK,cAAc;EACpD,IAAI,YAAY,KAAK,UAAU;EAC/B,OAAO,MAAM;CACf,OAAO,IAAI,aAAa;EACtB,MAAM,cAAc,YAAY,eAAe,aAAa;EAC5D,IAAI,eAAe,GACjB,IAAI,UAAU;OACT;GAGL,OAAO,MAAM,YAAY;GACzB,IAAI,SAAS,IAAI,YAAY,YAAY,KAAK,WAAW;EAC3D;CACF,OAAO,IAAI,IAAA,SAET,IAAI,UAAU;CAMhB,MAAM,YAAY,OAAO,IAAI,SAAS,KAAK,KAAA;CAC3C,MAAM,iBAAiB,IAAA,eAAuB,KAAA;CAC9C,IAAI,aAAa,cAAc,gBAAgB;EAC7C,IAAI,gBAAgB,WAAW,cAAc;EAC7C,OAAO,UAAU;CACnB,OAAO,IAAI,gBACT,OAAO,UAAU;CAGnB,OAAO;AACT;;AAGA,SAAgB,mBAAmB,SAAgC;CACjE,OAAO,QAAQ,KAAK,WAAW,mBAAmB,MAAM,CAAC;AAC3D"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/click-id/index.ts"],"sourcesContent":["import { type SetCookie, parseCookie, stringifySetCookie } from 'cookie';\n\n/**\n * Server-side resolution of ad-click-id cookies (`_fbc`, `_rdt_cid`) from the incoming request.\n *\n * This is the framework-agnostic core meant to run on the *document* response (e.g. TanStack Start\n * server middleware, Next middleware). Setting `_fbc` via an HTTP `Set-Cookie` header on the top\n * document — rather than `document.cookie` on the client — is what Meta officially recommends and is\n * the only reliable way to keep the cookie alive for 90 days in Safari: ITP caps JavaScript-set\n * cookies on a fbclid-decorated landing page to 24 hours, and the document response is never\n * classified as CNAME/IP cloaking (it is the reference the browser measures cloaking against).\n *\n * reference: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc\n */\n\n// Meta's recommended _fbc cookie expiry. Events Manager warns (\"Server sending expired fbclid\")\n// when the embedded creationTime is older than 90 days and match quality/attribution may degrade;\n// no hard drop is documented.\nconst FBC_TTL_MS = 90 * 24 * 60 * 60 * 1000;\nconst RDT_CID_TTL_MS = 90 * 24 * 60 * 60 * 1000;\n// Tolerate a little clock skew when validating a creationTime against \"now\".\nconst CLOCK_SKEW_MS = 24 * 60 * 60 * 1000;\n\nexport const FBC_COOKIE = '_fbc';\nexport const RDT_CID_COOKIE = '_rdt_cid';\n\nexport type ParsedFbc = { raw: string; creationTime: number; fbclid: string };\n\n/**\n * Parse `fb.<subdomainIndex>.<creationTime>.<fbclid>`.\n * Returns undefined for anything malformed — a fbclid never contains a dot, but joining the tail\n * back together keeps us forward-compatible if that ever changes.\n */\nexport function parseFbc(\n raw: string | undefined | null,\n now: number = Date.now()\n): ParsedFbc | undefined {\n if (!raw) return undefined;\n const parts = raw.split('.');\n if (parts.length < 4 || parts[0] !== 'fb') return undefined;\n\n const creationTime = Number(parts[2]);\n const fbclid = parts.slice(3).join('.');\n if (!fbclid) return undefined;\n // creationTime is UNIX ms; reject seconds-precision or future-dated values as malformed.\n if (!Number.isFinite(creationTime)) return undefined;\n if (creationTime < 1e12 || creationTime > now + CLOCK_SKEW_MS) return undefined;\n\n return { raw, creationTime, fbclid };\n}\n\n/**\n * Build a fresh `_fbc` value. subdomainIndex is the cookie's domain level (com=0, example.com=1,\n * www.example.com=2); 1 is correct for an apex-hosted cookie.\n */\nexport function formatFbc(fbclid: string, now: number, subdomainIndex = 1): string {\n return `fb.${subdomainIndex}.${now}.${fbclid}`;\n}\n\nexport type ResolveClickIdCookiesInput = {\n /** The absolute request URL (must include the query string). */\n url: string;\n /** The raw `Cookie` request header, if any. */\n cookieHeader?: string | null;\n /** Overridable clock, primarily for tests. Defaults to `Date.now()`. */\n now?: number;\n /** `Domain` attribute for the emitted cookies, e.g. `.shware.io`. Omit for a host-only cookie. */\n domain?: string;\n /** `Secure` attribute, default true. Set false only for local http testing. */\n secure?: boolean;\n /** subdomainIndex for a freshly built `_fbc` (see {@link formatFbc}). Default 1. */\n subdomainIndex?: number;\n /**\n * Re-issue a still-valid `_fbc` at its *remaining* lifetime on every call. On by default, as an\n * ITP self-heal: cookies follow last-writer-wins, so if the Meta Pixel overwrote `_fbc` via\n * `document.cookie` (Safari caps JS-written cookies on a fbclid-decorated landing to 24h),\n * re-issuing the long-lived HTTP cookie on the next navigation restores it — the same\n * continuous-re-issue mitigation the sGTM ecosystem uses (stape Cookie Keeper, Cookie Monster).\n * The value (and its creationTime) is byte-for-byte identical and the window never slides, so it\n * cannot trigger Meta's expired/modified-fbclid warnings.\n *\n * Set false to strictly follow Meta's documented conditional-write rule (\"only set the cookie if\n * it doesn't exist or the fbclid changed\") — e.g. to keep pages CDN-cacheable, since the\n * re-issue attaches a per-user `Set-Cookie` (forcing `no-store`) to every response carrying an\n * `_fbc`.\n */\n refresh?: boolean;\n};\n\nexport type ResolveClickIdCookiesResult = {\n /**\n * Cookies to emit on the response, ready for `Set-Cookie`. Empty when nothing needs to change.\n * A `value: ''`, `maxAge: 0` entry is a deletion (an expired or malformed leftover).\n */\n cookies: SetCookie[];\n /** The resolved `_fbc` value, for immediate server-side use (e.g. Conversions API). */\n fbc?: string;\n /** The resolved `_rdt_cid` value. */\n rdt_cid?: string;\n};\n\nfunction searchParams(url: string): URLSearchParams {\n try {\n return new URL(url).searchParams;\n } catch {\n // Fall back to a manual split so a relative or slightly malformed URL still yields the query.\n const q = url.indexOf('?');\n return new URLSearchParams(q !== -1 ? url.slice(q + 1) : '');\n }\n}\n\n/**\n * Resolve the click-id cookies to set on the current document response.\n *\n * `_fbc` follows Meta's documented conditional-write rule value-wise: a new fbclid (or an absent\n * cookie) opens a fresh 90-day window; a same-fbclid cookie keeps its original value and\n * `creationTime`. Expired (>90d) or malformed values are cleared instead of forwarded. By default,\n * ({@link ResolveClickIdCookiesInput.refresh}) a still-valid cookie is re-issued unchanged at its\n * *remaining* lifetime — never `now + 90d` — so a returning visitor's window cannot slide forward\n * (which is what makes Meta flag an expired fbclid).\n */\nexport function resolveClickIdCookies(\n input: ResolveClickIdCookiesInput\n): ResolveClickIdCookiesResult {\n const { url, cookieHeader, domain, secure = true, subdomainIndex = 1, refresh = true } = input;\n const now = input.now ?? Date.now();\n\n const params = searchParams(url);\n const jar = parseCookie(cookieHeader ?? '');\n const cookies: SetCookie[] = [];\n const result: ResolveClickIdCookiesResult = { cookies };\n\n const base = { path: '/', secure, sameSite: 'lax', domain } as const;\n const set = (name: string, value: string, ttlMs: number) =>\n cookies.push({ name, value, maxAge: Math.floor(ttlMs / 1000), ...base });\n const del = (name: string) => cookies.push({ name, value: '', maxAge: 0, ...base });\n\n // --- Meta _fbc ---\n const urlFbclid = params.get('fbclid') || undefined;\n const existingFbc = parseFbc(jar[FBC_COOKIE], now);\n\n if (urlFbclid && urlFbclid !== existingFbc?.fbclid) {\n // A new click always wins and opens a fresh 90-day window.\n const raw = formatFbc(urlFbclid, now, subdomainIndex);\n set(FBC_COOKIE, raw, FBC_TTL_MS);\n result.fbc = raw;\n } else if (existingFbc) {\n const remainingMs = existingFbc.creationTime + FBC_TTL_MS - now;\n if (remainingMs <= 0) {\n del(FBC_COOKIE);\n } else {\n // Same fbclid: the value and creationTime are preserved (Meta's rule); refresh (default)\n // re-issues it unchanged at its remaining lifetime as an ITP self-heal.\n result.fbc = existingFbc.raw;\n if (refresh) set(FBC_COOKIE, existingFbc.raw, remainingMs);\n }\n } else if (jar[FBC_COOKIE]) {\n // Malformed leftover — clear it rather than forwarding it to Meta.\n del(FBC_COOKIE);\n }\n\n // --- Reddit _rdt_cid ---\n // No embedded timestamp, so it can only be anchored at first capture; set it once from the URL and\n // otherwise leave the existing cookie untouched (re-issuing would slide its window).\n const urlRdtCid = params.get('rdt_cid') || undefined;\n const existingRdtCid = jar[RDT_CID_COOKIE] || undefined;\n if (urlRdtCid && urlRdtCid !== existingRdtCid) {\n set(RDT_CID_COOKIE, urlRdtCid, RDT_CID_TTL_MS);\n result.rdt_cid = urlRdtCid;\n } else if (existingRdtCid) {\n result.rdt_cid = existingRdtCid;\n }\n\n return result;\n}\n\n/** Serialize the resolved cookies into `Set-Cookie` header values. */\nexport function toSetCookieHeaders(cookies: SetCookie[]): string[] {\n return cookies.map((cookie) => stringifySetCookie(cookie));\n}\n"],"mappings":";;;;;;;;;;;;;;AAkBA,MAAM,aAAa,OAAU,KAAK,KAAK;AACvC,MAAM,iBAAiB,OAAU,KAAK,KAAK;AAE3C,MAAM,gBAAgB,OAAU,KAAK;AAErC,MAAa,aAAa;AAC1B,MAAa,iBAAiB;;;;;;AAS9B,SAAgB,SACd,KACA,MAAc,KAAK,IAAI,GACA;CACvB,IAAI,CAAC,KAAK,OAAO,KAAA;CACjB,MAAM,QAAQ,IAAI,MAAM,GAAG;CAC3B,IAAI,MAAM,SAAS,KAAK,MAAM,OAAO,MAAM,OAAO,KAAA;CAElD,MAAM,eAAe,OAAO,MAAM,EAAE;CACpC,MAAM,SAAS,MAAM,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG;CACtC,IAAI,CAAC,QAAQ,OAAO,KAAA;CAEpB,IAAI,CAAC,OAAO,SAAS,YAAY,GAAG,OAAO,KAAA;CAC3C,IAAI,eAAe,gBAAQ,eAAe,MAAM,eAAe,OAAO,KAAA;CAEtE,OAAO;EAAE;EAAK;EAAc;CAAO;AACrC;;;;;AAMA,SAAgB,UAAU,QAAgB,KAAa,iBAAiB,GAAW;CACjF,OAAO,MAAM,eAAe,GAAG,IAAI,GAAG;AACxC;AA4CA,SAAS,aAAa,KAA8B;CAClD,IAAI;EACF,OAAO,IAAI,IAAI,GAAG,CAAC,CAAC;CACtB,QAAQ;EAEN,MAAM,IAAI,IAAI,QAAQ,GAAG;EACzB,OAAO,IAAI,gBAAgB,MAAM,KAAK,IAAI,MAAM,IAAI,CAAC,IAAI,EAAE;CAC7D;AACF;;;;;;;;;;;AAYA,SAAgB,sBACd,OAC6B;CAC7B,MAAM,EAAE,KAAK,cAAc,QAAQ,SAAS,MAAM,iBAAiB,GAAG,UAAU,SAAS;CACzF,MAAM,MAAM,MAAM,OAAO,KAAK,IAAI;CAElC,MAAM,SAAS,aAAa,GAAG;CAC/B,MAAM,MAAM,YAAY,gBAAgB,EAAE;CAC1C,MAAM,UAAuB,CAAC;CAC9B,MAAM,SAAsC,EAAE,QAAQ;CAEtD,MAAM,OAAO;EAAE,MAAM;EAAK;EAAQ,UAAU;EAAO;CAAO;CAC1D,MAAM,OAAO,MAAc,OAAe,UACxC,QAAQ,KAAK;EAAE;EAAM;EAAO,QAAQ,KAAK,MAAM,QAAQ,GAAI;EAAG,GAAG;CAAK,CAAC;CACzE,MAAM,OAAO,SAAiB,QAAQ,KAAK;EAAE;EAAM,OAAO;EAAI,QAAQ;EAAG,GAAG;CAAK,CAAC;CAGlF,MAAM,YAAY,OAAO,IAAI,QAAQ,KAAK,KAAA;CAC1C,MAAM,cAAc,SAAS,IAAI,aAAa,GAAG;CAEjD,IAAI,aAAa,cAAc,aAAa,QAAQ;EAElD,MAAM,MAAM,UAAU,WAAW,KAAK,cAAc;EACpD,IAAI,YAAY,KAAK,UAAU;EAC/B,OAAO,MAAM;CACf,OAAO,IAAI,aAAa;EACtB,MAAM,cAAc,YAAY,eAAe,aAAa;EAC5D,IAAI,eAAe,GACjB,IAAI,UAAU;OACT;GAGL,OAAO,MAAM,YAAY;GACzB,IAAI,SAAS,IAAI,YAAY,YAAY,KAAK,WAAW;EAC3D;CACF,OAAO,IAAI,IAAA,SAET,IAAI,UAAU;CAMhB,MAAM,YAAY,OAAO,IAAI,SAAS,KAAK,KAAA;CAC3C,MAAM,iBAAiB,IAAA,eAAuB,KAAA;CAC9C,IAAI,aAAa,cAAc,gBAAgB;EAC7C,IAAI,gBAAgB,WAAW,cAAc;EAC7C,OAAO,UAAU;CACnB,OAAO,IAAI,gBACT,OAAO,UAAU;CAGnB,OAAO;AACT;;AAGA,SAAgB,mBAAmB,SAAgC;CACjE,OAAO,QAAQ,KAAK,WAAW,mBAAmB,MAAM,CAAC;AAC3D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.cjs","names":[],"sources":["../../src/constants/storage.ts"],"sourcesContent":["export const keys = {\n device_id: 'device_id',\n visitor_id: 'visitor_id',\n first_open_time: 'first_open_time',\n first_visit_time: 'first_visit_time',\n} as const;\n"],"mappings":";;AAAA,MAAa,OAAO;CAClB,WAAW;CACX,YAAY;CACZ,iBAAiB;CACjB,kBAAkB;
|
|
1
|
+
{"version":3,"file":"storage.cjs","names":[],"sources":["../../src/constants/storage.ts"],"sourcesContent":["export const keys = {\n device_id: 'device_id',\n visitor_id: 'visitor_id',\n first_open_time: 'first_open_time',\n first_visit_time: 'first_visit_time',\n session: 'session',\n} as const;\n"],"mappings":";;AAAA,MAAa,OAAO;CAClB,WAAW;CACX,YAAY;CACZ,iBAAiB;CACjB,kBAAkB;CAClB,SAAS;AACX"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.cts","names":[],"sources":["../../src/constants/storage.ts"],"mappings":";cAAa;WACX;WACA;WACA;WACA"}
|
|
1
|
+
{"version":3,"file":"storage.d.cts","names":[],"sources":["../../src/constants/storage.ts"],"mappings":";cAAa;WACX;WACA;WACA;WACA;WACA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.mts","names":[],"sources":["../../src/constants/storage.ts"],"mappings":";cAAa;WACX;WACA;WACA;WACA"}
|
|
1
|
+
{"version":3,"file":"storage.d.mts","names":[],"sources":["../../src/constants/storage.ts"],"mappings":";cAAa;WACX;WACA;WACA;WACA;WACA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.mjs","names":[],"sources":["../../src/constants/storage.ts"],"sourcesContent":["export const keys = {\n device_id: 'device_id',\n visitor_id: 'visitor_id',\n first_open_time: 'first_open_time',\n first_visit_time: 'first_visit_time',\n} as const;\n"],"mappings":";AAAA,MAAa,OAAO;CAClB,WAAW;CACX,YAAY;CACZ,iBAAiB;CACjB,kBAAkB;
|
|
1
|
+
{"version":3,"file":"storage.mjs","names":[],"sources":["../../src/constants/storage.ts"],"sourcesContent":["export const keys = {\n device_id: 'device_id',\n visitor_id: 'visitor_id',\n first_open_time: 'first_open_time',\n first_visit_time: 'first_visit_time',\n session: 'session',\n} as const;\n"],"mappings":";AAAA,MAAa,OAAO;CAClB,WAAW;CACX,YAAY;CACZ,iBAAiB;CACjB,kBAAkB;CAClB,SAAS;AACX"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_setup_index = require("../setup/index.cjs");
|
|
3
|
-
const require_setup_session = require("../setup/session.cjs");
|
|
4
3
|
const require_constants_storage = require("../constants/storage.cjs");
|
|
4
|
+
const require_setup_session = require("../setup/session.cjs");
|
|
5
5
|
const require_track_index = require("../track/index.cjs");
|
|
6
6
|
const require_hooks_use_previous = require("./use-previous.cjs");
|
|
7
7
|
let react = require("react");
|
|
@@ -28,7 +28,6 @@ function useAppAnalytics(pathname) {
|
|
|
28
28
|
(0, react.useEffect)(() => {
|
|
29
29
|
const session = require_setup_session.getSession();
|
|
30
30
|
sendFirstOpen(pathname);
|
|
31
|
-
require_track_index.track("session_start", void 0);
|
|
32
31
|
const subscription = react_native.AppState.addEventListener("change", (state) => {
|
|
33
32
|
session.updateAccumulator();
|
|
34
33
|
if (state === "active" && !session.isActive()) session.updateActive(true);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-app-analytics.cjs","names":["config","keys","getSession","usePrevious","AppState"],"sources":["../../src/hooks/use-app-analytics.ts"],"sourcesContent":["import { useEffect } from 'react';\nimport { AppState } from 'react-native';\nimport { keys } from '../constants/storage';\nimport { config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { track } from '../track/index';\nimport { usePrevious } from './use-previous';\n\nfunction sendFirstOpen(pathname: string) {\n if (config.storage.getItem(keys.first_open_time)) return;\n track('first_open', { screen_name: pathname, screen_class: pathname });\n config.storage.setItem(keys.first_open_time, new Date().toISOString());\n}\n\nfunction sendUserEngagement() {\n const engagement_time_msec = getSession().flush();\n if (engagement_time_msec <= 0) return;\n track('user_engagement', { engagement_time_msec, trigger: 'background' });\n}\n\nexport function useAppAnalytics(pathname: string) {\n const prevPathname = usePrevious(pathname);\n\n useEffect(() => {\n const session = getSession();\n\n sendFirstOpen(pathname);\n
|
|
1
|
+
{"version":3,"file":"use-app-analytics.cjs","names":["config","keys","getSession","usePrevious","AppState"],"sources":["../../src/hooks/use-app-analytics.ts"],"sourcesContent":["import { useEffect } from 'react';\nimport { AppState } from 'react-native';\nimport { keys } from '../constants/storage';\nimport { config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { track } from '../track/index';\nimport { usePrevious } from './use-previous';\n\nfunction sendFirstOpen(pathname: string) {\n if (config.storage.getItem(keys.first_open_time)) return;\n track('first_open', { screen_name: pathname, screen_class: pathname });\n config.storage.setItem(keys.first_open_time, new Date().toISOString());\n}\n\nfunction sendUserEngagement() {\n const engagement_time_msec = getSession().flush();\n if (engagement_time_msec <= 0) return;\n track('user_engagement', { engagement_time_msec, trigger: 'background' });\n}\n\nexport function useAppAnalytics(pathname: string) {\n const prevPathname = usePrevious(pathname);\n\n useEffect(() => {\n const session = getSession();\n\n sendFirstOpen(pathname);\n\n const subscription = AppState.addEventListener('change', (state) => {\n session.updateAccumulator();\n // when returning to the foreground from the background\n if (state === 'active' && !session.isActive()) {\n session.updateActive(true);\n }\n // when entering the background\n else if (state !== 'active' && session.isActive()) {\n session.updateActive(false);\n sendUserEngagement();\n }\n });\n\n return () => subscription.remove();\n }, []);\n\n // when the screen is switched, the engagement time of the previous screen is recorded\n useEffect(() => {\n track('screen_view', {\n screen_name: pathname,\n screen_class: pathname,\n previous_screen_class: prevPathname ?? undefined,\n engagement_time_msec: prevPathname ? getSession().flush() : undefined,\n });\n }, [pathname]);\n}\n"],"mappings":";;;;;;;;;AAQA,SAAS,cAAc,UAAkB;CACvC,IAAIA,oBAAAA,OAAO,QAAQ,QAAQC,0BAAAA,KAAK,eAAe,GAAG;CAClD,oBAAA,MAAM,cAAc;EAAE,aAAa;EAAU,cAAc;CAAS,CAAC;CACrE,oBAAA,OAAO,QAAQ,QAAQA,0BAAAA,KAAK,kCAAiB,IAAI,KAAK,EAAA,CAAE,YAAY,CAAC;AACvE;AAEA,SAAS,qBAAqB;CAC5B,MAAM,uBAAuBC,sBAAAA,WAAW,CAAC,CAAC,MAAM;CAChD,IAAI,wBAAwB,GAAG;CAC/B,oBAAA,MAAM,mBAAmB;EAAE;EAAsB,SAAS;CAAa,CAAC;AAC1E;AAEA,SAAgB,gBAAgB,UAAkB;CAChD,MAAM,eAAeC,2BAAAA,YAAY,QAAQ;CAEzC,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,MAAM,UAAUD,sBAAAA,WAAW;EAE3B,cAAc,QAAQ;EAEtB,MAAM,eAAeE,aAAAA,SAAS,iBAAiB,WAAW,UAAU;GAClE,QAAQ,kBAAkB;GAE1B,IAAI,UAAU,YAAY,CAAC,QAAQ,SAAS,GAC1C,QAAQ,aAAa,IAAI;QAGtB,IAAI,UAAU,YAAY,QAAQ,SAAS,GAAG;IACjD,QAAQ,aAAa,KAAK;IAC1B,mBAAmB;GACrB;EACF,CAAC;EAED,aAAa,aAAa,OAAO;CACnC,GAAG,CAAC,CAAC;CAGL,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,oBAAA,MAAM,eAAe;GACnB,aAAa;GACb,cAAc;GACd,uBAAuB,gBAAgB,KAAA;GACvC,sBAAsB,eAAeF,sBAAAA,WAAW,CAAC,CAAC,MAAM,IAAI,KAAA;EAC9D,CAAC;CACH,GAAG,CAAC,QAAQ,CAAC;AACf"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { config } from "../setup/index.mjs";
|
|
2
|
-
import { getSession } from "../setup/session.mjs";
|
|
3
2
|
import { keys } from "../constants/storage.mjs";
|
|
3
|
+
import { getSession } from "../setup/session.mjs";
|
|
4
4
|
import { track } from "../track/index.mjs";
|
|
5
5
|
import { usePrevious } from "./use-previous.mjs";
|
|
6
6
|
import { useEffect } from "react";
|
|
@@ -27,7 +27,6 @@ function useAppAnalytics(pathname) {
|
|
|
27
27
|
useEffect(() => {
|
|
28
28
|
const session = getSession();
|
|
29
29
|
sendFirstOpen(pathname);
|
|
30
|
-
track("session_start", void 0);
|
|
31
30
|
const subscription = AppState.addEventListener("change", (state) => {
|
|
32
31
|
session.updateAccumulator();
|
|
33
32
|
if (state === "active" && !session.isActive()) session.updateActive(true);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-app-analytics.mjs","names":[],"sources":["../../src/hooks/use-app-analytics.ts"],"sourcesContent":["import { useEffect } from 'react';\nimport { AppState } from 'react-native';\nimport { keys } from '../constants/storage';\nimport { config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { track } from '../track/index';\nimport { usePrevious } from './use-previous';\n\nfunction sendFirstOpen(pathname: string) {\n if (config.storage.getItem(keys.first_open_time)) return;\n track('first_open', { screen_name: pathname, screen_class: pathname });\n config.storage.setItem(keys.first_open_time, new Date().toISOString());\n}\n\nfunction sendUserEngagement() {\n const engagement_time_msec = getSession().flush();\n if (engagement_time_msec <= 0) return;\n track('user_engagement', { engagement_time_msec, trigger: 'background' });\n}\n\nexport function useAppAnalytics(pathname: string) {\n const prevPathname = usePrevious(pathname);\n\n useEffect(() => {\n const session = getSession();\n\n sendFirstOpen(pathname);\n
|
|
1
|
+
{"version":3,"file":"use-app-analytics.mjs","names":[],"sources":["../../src/hooks/use-app-analytics.ts"],"sourcesContent":["import { useEffect } from 'react';\nimport { AppState } from 'react-native';\nimport { keys } from '../constants/storage';\nimport { config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { track } from '../track/index';\nimport { usePrevious } from './use-previous';\n\nfunction sendFirstOpen(pathname: string) {\n if (config.storage.getItem(keys.first_open_time)) return;\n track('first_open', { screen_name: pathname, screen_class: pathname });\n config.storage.setItem(keys.first_open_time, new Date().toISOString());\n}\n\nfunction sendUserEngagement() {\n const engagement_time_msec = getSession().flush();\n if (engagement_time_msec <= 0) return;\n track('user_engagement', { engagement_time_msec, trigger: 'background' });\n}\n\nexport function useAppAnalytics(pathname: string) {\n const prevPathname = usePrevious(pathname);\n\n useEffect(() => {\n const session = getSession();\n\n sendFirstOpen(pathname);\n\n const subscription = AppState.addEventListener('change', (state) => {\n session.updateAccumulator();\n // when returning to the foreground from the background\n if (state === 'active' && !session.isActive()) {\n session.updateActive(true);\n }\n // when entering the background\n else if (state !== 'active' && session.isActive()) {\n session.updateActive(false);\n sendUserEngagement();\n }\n });\n\n return () => subscription.remove();\n }, []);\n\n // when the screen is switched, the engagement time of the previous screen is recorded\n useEffect(() => {\n track('screen_view', {\n screen_name: pathname,\n screen_class: pathname,\n previous_screen_class: prevPathname ?? undefined,\n engagement_time_msec: prevPathname ? getSession().flush() : undefined,\n });\n }, [pathname]);\n}\n"],"mappings":";;;;;;;;AAQA,SAAS,cAAc,UAAkB;CACvC,IAAI,OAAO,QAAQ,QAAQ,KAAK,eAAe,GAAG;CAClD,MAAM,cAAc;EAAE,aAAa;EAAU,cAAc;CAAS,CAAC;CACrE,OAAO,QAAQ,QAAQ,KAAK,kCAAiB,IAAI,KAAK,EAAA,CAAE,YAAY,CAAC;AACvE;AAEA,SAAS,qBAAqB;CAC5B,MAAM,uBAAuB,WAAW,CAAC,CAAC,MAAM;CAChD,IAAI,wBAAwB,GAAG;CAC/B,MAAM,mBAAmB;EAAE;EAAsB,SAAS;CAAa,CAAC;AAC1E;AAEA,SAAgB,gBAAgB,UAAkB;CAChD,MAAM,eAAe,YAAY,QAAQ;CAEzC,gBAAgB;EACd,MAAM,UAAU,WAAW;EAE3B,cAAc,QAAQ;EAEtB,MAAM,eAAe,SAAS,iBAAiB,WAAW,UAAU;GAClE,QAAQ,kBAAkB;GAE1B,IAAI,UAAU,YAAY,CAAC,QAAQ,SAAS,GAC1C,QAAQ,aAAa,IAAI;QAGtB,IAAI,UAAU,YAAY,QAAQ,SAAS,GAAG;IACjD,QAAQ,aAAa,KAAK;IAC1B,mBAAmB;GACrB;EACF,CAAC;EAED,aAAa,aAAa,OAAO;CACnC,GAAG,CAAC,CAAC;CAGL,gBAAgB;EACd,MAAM,eAAe;GACnB,aAAa;GACb,cAAc;GACd,uBAAuB,gBAAgB,KAAA;GACvC,sBAAsB,eAAe,WAAW,CAAC,CAAC,MAAM,IAAI,KAAA;EAC9D,CAAC;CACH,GAAG,CAAC,QAAQ,CAAC;AACf"}
|
|
@@ -17,7 +17,7 @@ function useOutboundClickAnalytics() {
|
|
|
17
17
|
outbound: true,
|
|
18
18
|
link_id: anchor.id || "",
|
|
19
19
|
link_url: anchor.href,
|
|
20
|
-
link_text: anchor.textContent.trim()
|
|
20
|
+
link_text: anchor.textContent.trim().slice(0, 512),
|
|
21
21
|
link_domain: url.hostname,
|
|
22
22
|
link_classes: anchor.className || ""
|
|
23
23
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-outbound-click-analytics.cjs","names":[],"sources":["../../src/hooks/use-outbound-click-analytics.ts"],"sourcesContent":["import { useEffect } from 'react';\nimport { track } from '../track/index';\n\n/**\n * Tracks outbound link clicks - when a user clicks a link that leads away\n * from the current domain to another website.\n */\nexport function useOutboundClickAnalytics() {\n useEffect(() => {\n const onClick = (event: MouseEvent) => {\n // Find the closest anchor element from the clicked target\n const target = event.target as HTMLElement;\n const anchor = target.closest('a');\n if (!anchor?.href) return;\n\n try {\n const url = new URL(anchor.href, window.location.origin);\n\n // Check if it's an external link (different hostname)\n if (url.hostname !== window.location.hostname) {\n track('click', {\n outbound: true,\n link_id: anchor.id || '',\n link_url: anchor.href,\n link_text: anchor.textContent.trim()
|
|
1
|
+
{"version":3,"file":"use-outbound-click-analytics.cjs","names":[],"sources":["../../src/hooks/use-outbound-click-analytics.ts"],"sourcesContent":["import { useEffect } from 'react';\nimport { track } from '../track/index';\n\n/**\n * Tracks outbound link clicks - when a user clicks a link that leads away\n * from the current domain to another website.\n */\nexport function useOutboundClickAnalytics() {\n useEffect(() => {\n const onClick = (event: MouseEvent) => {\n // Find the closest anchor element from the clicked target\n const target = event.target as HTMLElement;\n const anchor = target.closest('a');\n if (!anchor?.href) return;\n\n try {\n const url = new URL(anchor.href, window.location.origin);\n\n // Check if it's an external link (different hostname)\n if (url.hostname !== window.location.hostname) {\n track('click', {\n outbound: true,\n link_id: anchor.id || '',\n link_url: anchor.href,\n // An anchor can wrap a whole card, so its text runs to kilobytes of markup content\n // that no report ever looks at. 100 characters is what GA4 keeps of a text event\n // parameter, and the transport limit is well above it either way.\n link_text: anchor.textContent.trim().slice(0, 512),\n link_domain: url.hostname,\n link_classes: anchor.className || '',\n });\n }\n } catch {\n // Invalid URL, ignore\n }\n };\n\n document.addEventListener('click', onClick, { passive: true, capture: true });\n return () => document.removeEventListener('click', onClick, { capture: true });\n }, []);\n}\n"],"mappings":";;;;;;;;AAOA,SAAgB,4BAA4B;CAC1C,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,MAAM,WAAW,UAAsB;GAGrC,MAAM,SADS,MAAM,OACC,QAAQ,GAAG;GACjC,IAAI,CAAC,QAAQ,MAAM;GAEnB,IAAI;IACF,MAAM,MAAM,IAAI,IAAI,OAAO,MAAM,OAAO,SAAS,MAAM;IAGvD,IAAI,IAAI,aAAa,OAAO,SAAS,UACnC,oBAAA,MAAM,SAAS;KACb,UAAU;KACV,SAAS,OAAO,MAAM;KACtB,UAAU,OAAO;KAIjB,WAAW,OAAO,YAAY,KAAK,CAAC,CAAC,MAAM,GAAG,GAAG;KACjD,aAAa,IAAI;KACjB,cAAc,OAAO,aAAa;IACpC,CAAC;GAEL,QAAQ,CAER;EACF;EAEA,SAAS,iBAAiB,SAAS,SAAS;GAAE,SAAS;GAAM,SAAS;EAAK,CAAC;EAC5E,aAAa,SAAS,oBAAoB,SAAS,SAAS,EAAE,SAAS,KAAK,CAAC;CAC/E,GAAG,CAAC,CAAC;AACP"}
|
|
@@ -16,7 +16,7 @@ function useOutboundClickAnalytics() {
|
|
|
16
16
|
outbound: true,
|
|
17
17
|
link_id: anchor.id || "",
|
|
18
18
|
link_url: anchor.href,
|
|
19
|
-
link_text: anchor.textContent.trim()
|
|
19
|
+
link_text: anchor.textContent.trim().slice(0, 512),
|
|
20
20
|
link_domain: url.hostname,
|
|
21
21
|
link_classes: anchor.className || ""
|
|
22
22
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-outbound-click-analytics.mjs","names":[],"sources":["../../src/hooks/use-outbound-click-analytics.ts"],"sourcesContent":["import { useEffect } from 'react';\nimport { track } from '../track/index';\n\n/**\n * Tracks outbound link clicks - when a user clicks a link that leads away\n * from the current domain to another website.\n */\nexport function useOutboundClickAnalytics() {\n useEffect(() => {\n const onClick = (event: MouseEvent) => {\n // Find the closest anchor element from the clicked target\n const target = event.target as HTMLElement;\n const anchor = target.closest('a');\n if (!anchor?.href) return;\n\n try {\n const url = new URL(anchor.href, window.location.origin);\n\n // Check if it's an external link (different hostname)\n if (url.hostname !== window.location.hostname) {\n track('click', {\n outbound: true,\n link_id: anchor.id || '',\n link_url: anchor.href,\n link_text: anchor.textContent.trim()
|
|
1
|
+
{"version":3,"file":"use-outbound-click-analytics.mjs","names":[],"sources":["../../src/hooks/use-outbound-click-analytics.ts"],"sourcesContent":["import { useEffect } from 'react';\nimport { track } from '../track/index';\n\n/**\n * Tracks outbound link clicks - when a user clicks a link that leads away\n * from the current domain to another website.\n */\nexport function useOutboundClickAnalytics() {\n useEffect(() => {\n const onClick = (event: MouseEvent) => {\n // Find the closest anchor element from the clicked target\n const target = event.target as HTMLElement;\n const anchor = target.closest('a');\n if (!anchor?.href) return;\n\n try {\n const url = new URL(anchor.href, window.location.origin);\n\n // Check if it's an external link (different hostname)\n if (url.hostname !== window.location.hostname) {\n track('click', {\n outbound: true,\n link_id: anchor.id || '',\n link_url: anchor.href,\n // An anchor can wrap a whole card, so its text runs to kilobytes of markup content\n // that no report ever looks at. 100 characters is what GA4 keeps of a text event\n // parameter, and the transport limit is well above it either way.\n link_text: anchor.textContent.trim().slice(0, 512),\n link_domain: url.hostname,\n link_classes: anchor.className || '',\n });\n }\n } catch {\n // Invalid URL, ignore\n }\n };\n\n document.addEventListener('click', onClick, { passive: true, capture: true });\n return () => document.removeEventListener('click', onClick, { capture: true });\n }, []);\n}\n"],"mappings":";;;;;;;AAOA,SAAgB,4BAA4B;CAC1C,gBAAgB;EACd,MAAM,WAAW,UAAsB;GAGrC,MAAM,SADS,MAAM,OACC,QAAQ,GAAG;GACjC,IAAI,CAAC,QAAQ,MAAM;GAEnB,IAAI;IACF,MAAM,MAAM,IAAI,IAAI,OAAO,MAAM,OAAO,SAAS,MAAM;IAGvD,IAAI,IAAI,aAAa,OAAO,SAAS,UACnC,MAAM,SAAS;KACb,UAAU;KACV,SAAS,OAAO,MAAM;KACtB,UAAU,OAAO;KAIjB,WAAW,OAAO,YAAY,KAAK,CAAC,CAAC,MAAM,GAAG,GAAG;KACjD,aAAa,IAAI;KACjB,cAAc,OAAO,aAAa;IACpC,CAAC;GAEL,QAAQ,CAER;EACF;EAEA,SAAS,iBAAiB,SAAS,SAAS;GAAE,SAAS;GAAM,SAAS;EAAK,CAAC;EAC5E,aAAa,SAAS,oBAAoB,SAAS,SAAS,EAAE,SAAS,KAAK,CAAC;CAC/E,GAAG,CAAC,CAAC;AACP"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_setup_index = require("../setup/index.cjs");
|
|
3
|
-
const require_setup_session = require("../setup/session.cjs");
|
|
4
3
|
const require_constants_storage = require("../constants/storage.cjs");
|
|
4
|
+
const require_setup_session = require("../setup/session.cjs");
|
|
5
5
|
const require_track_index = require("../track/index.cjs");
|
|
6
6
|
const require_hooks_use_previous = require("./use-previous.cjs");
|
|
7
7
|
let _shware_utils = require("@shware/utils");
|
|
@@ -59,7 +59,6 @@ function useWebAnalytics(pathname) {
|
|
|
59
59
|
(0, react.useEffect)(() => {
|
|
60
60
|
const session = require_setup_session.getSession();
|
|
61
61
|
sendFirstVisit(pathname);
|
|
62
|
-
require_track_index.track("session_start", void 0);
|
|
63
62
|
const onScroll = (0, _shware_utils.throttle)(() => {
|
|
64
63
|
session.updateAccumulator();
|
|
65
64
|
if (hasSendScroll.current) return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-web-analytics.cjs","names":["config","keys","getSession","usePrevious"],"sources":["../../src/hooks/use-web-analytics.ts"],"sourcesContent":["import { throttle } from '@shware/utils';\nimport { useEffect, useRef } from 'react';\nimport { keys } from '../constants/storage';\nimport { config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { sendBeacon, track } from '../track/index';\nimport { usePrevious } from './use-previous';\n\nfunction sendFirstVisit(pathname: string) {\n if (config.storage.getItem(keys.first_visit_time)) return;\n track('first_visit', {\n page_path: pathname,\n page_title: document.title,\n page_referrer: document.referrer,\n page_location: window.location.href,\n });\n config.storage.setItem(keys.first_visit_time, new Date().toISOString());\n}\n\nfunction sendUserEngagement(trigger: 'pagehide' | 'visibilitychange') {\n const engagement_time_msec = getSession().flush();\n if (engagement_time_msec <= 0) return;\n sendBeacon('user_engagement', { engagement_time_msec, trigger });\n}\n\nfunction sendScroll() {\n const engagement_time_msec = getSession().flush();\n if (engagement_time_msec <= 0) return;\n track('scroll', { engagement_time_msec });\n}\n\nfunction getScrollPercent() {\n const scrollTop = window.scrollY || document.documentElement.scrollTop;\n const windowHeight = window.innerHeight;\n const docHeight = document.documentElement.scrollHeight;\n if (docHeight === 0) return 0;\n return ((scrollTop + windowHeight) * 100) / docHeight;\n}\n\nfunction onPageHide() {\n getSession().pagehide();\n sendUserEngagement('pagehide');\n}\n\nfunction onVisibilityChange() {\n getSession().visibilitychange(document.visibilityState);\n if (document.visibilityState === 'hidden') {\n sendUserEngagement('visibilitychange');\n }\n}\n\n/**\n * 1. send session_start event when the page is loaded\n * 2. send scroll event when the user scrolls more than 90% of the page\n * 3. send user_engagement event when the page is hidden or the user is not focused\n */\nexport function useWebAnalytics(pathname: string) {\n const prevPathname = usePrevious(pathname);\n\n // reset state when the pathname changes and send scroll when the user navigates to a new page\n const hasSendScroll = useRef(false);\n useEffect(() => {\n hasSendScroll.current = false;\n }, [pathname]);\n\n useEffect(() => {\n // One lookup for the whole effect: `addEventListener` and its matching\n // `removeEventListener` have to be handed the very same function.\n const session = getSession();\n\n sendFirstVisit(pathname);\n
|
|
1
|
+
{"version":3,"file":"use-web-analytics.cjs","names":["config","keys","getSession","usePrevious"],"sources":["../../src/hooks/use-web-analytics.ts"],"sourcesContent":["import { throttle } from '@shware/utils';\nimport { useEffect, useRef } from 'react';\nimport { keys } from '../constants/storage';\nimport { config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { sendBeacon, track } from '../track/index';\nimport { usePrevious } from './use-previous';\n\nfunction sendFirstVisit(pathname: string) {\n if (config.storage.getItem(keys.first_visit_time)) return;\n track('first_visit', {\n page_path: pathname,\n page_title: document.title,\n page_referrer: document.referrer,\n page_location: window.location.href,\n });\n config.storage.setItem(keys.first_visit_time, new Date().toISOString());\n}\n\nfunction sendUserEngagement(trigger: 'pagehide' | 'visibilitychange') {\n const engagement_time_msec = getSession().flush();\n if (engagement_time_msec <= 0) return;\n sendBeacon('user_engagement', { engagement_time_msec, trigger });\n}\n\nfunction sendScroll() {\n const engagement_time_msec = getSession().flush();\n if (engagement_time_msec <= 0) return;\n track('scroll', { engagement_time_msec });\n}\n\nfunction getScrollPercent() {\n const scrollTop = window.scrollY || document.documentElement.scrollTop;\n const windowHeight = window.innerHeight;\n const docHeight = document.documentElement.scrollHeight;\n if (docHeight === 0) return 0;\n return ((scrollTop + windowHeight) * 100) / docHeight;\n}\n\nfunction onPageHide() {\n getSession().pagehide();\n sendUserEngagement('pagehide');\n}\n\nfunction onVisibilityChange() {\n getSession().visibilitychange(document.visibilityState);\n if (document.visibilityState === 'hidden') {\n sendUserEngagement('visibilitychange');\n }\n}\n\n/**\n * 1. send session_start event when the page is loaded\n * 2. send scroll event when the user scrolls more than 90% of the page\n * 3. send user_engagement event when the page is hidden or the user is not focused\n */\nexport function useWebAnalytics(pathname: string) {\n const prevPathname = usePrevious(pathname);\n\n // reset state when the pathname changes and send scroll when the user navigates to a new page\n const hasSendScroll = useRef(false);\n useEffect(() => {\n hasSendScroll.current = false;\n }, [pathname]);\n\n useEffect(() => {\n // One lookup for the whole effect: `addEventListener` and its matching\n // `removeEventListener` have to be handed the very same function.\n const session = getSession();\n\n sendFirstVisit(pathname);\n\n const onScroll = throttle(() => {\n session.updateAccumulator();\n if (hasSendScroll.current) return;\n // only send scroll when the user has scrolled more than 90% of the page\n if (getScrollPercent() < 90) return;\n hasSendScroll.current = true;\n sendScroll();\n }, 500);\n\n const checkpointEvents = ['mousedown', 'keydown', 'touchstart'];\n const checkpoint = throttle(session.updateAccumulator, 1000);\n\n window.addEventListener('focus', session.focus);\n window.addEventListener('blur', session.blur);\n window.addEventListener('scroll', onScroll, { passive: true });\n window.addEventListener('pageshow', session.pageshow);\n window.addEventListener('pagehide', onPageHide);\n document.addEventListener('visibilitychange', onVisibilityChange);\n\n // save checkpoint\n checkpointEvents.forEach((e) => {\n window.addEventListener(e, checkpoint, { passive: true, capture: true });\n });\n\n return () => {\n window.removeEventListener('focus', session.focus);\n window.removeEventListener('blur', session.blur);\n window.removeEventListener('scroll', onScroll);\n window.removeEventListener('pageshow', session.pageshow);\n window.removeEventListener('pagehide', onPageHide);\n document.removeEventListener('visibilitychange', onVisibilityChange);\n checkpointEvents.forEach((e) => window.removeEventListener(e, checkpoint, { capture: true }));\n\n onScroll.cancel();\n checkpoint.cancel();\n };\n }, []);\n\n useEffect(() => {\n track('page_view', {\n page_path: pathname,\n page_title: document.title,\n page_referrer: document.referrer,\n page_location: window.location.href,\n previous_page_path: prevPathname ?? undefined,\n engagement_time_msec: prevPathname ? getSession().flush() : undefined,\n });\n }, [pathname]);\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { config } from "../setup/index.mjs";
|
|
2
|
-
import { getSession } from "../setup/session.mjs";
|
|
3
2
|
import { keys } from "../constants/storage.mjs";
|
|
3
|
+
import { getSession } from "../setup/session.mjs";
|
|
4
4
|
import { sendBeacon, track } from "../track/index.mjs";
|
|
5
5
|
import { usePrevious } from "./use-previous.mjs";
|
|
6
6
|
import { throttle } from "@shware/utils";
|
|
@@ -58,7 +58,6 @@ function useWebAnalytics(pathname) {
|
|
|
58
58
|
useEffect(() => {
|
|
59
59
|
const session = getSession();
|
|
60
60
|
sendFirstVisit(pathname);
|
|
61
|
-
track("session_start", void 0);
|
|
62
61
|
const onScroll = throttle(() => {
|
|
63
62
|
session.updateAccumulator();
|
|
64
63
|
if (hasSendScroll.current) return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-web-analytics.mjs","names":[],"sources":["../../src/hooks/use-web-analytics.ts"],"sourcesContent":["import { throttle } from '@shware/utils';\nimport { useEffect, useRef } from 'react';\nimport { keys } from '../constants/storage';\nimport { config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { sendBeacon, track } from '../track/index';\nimport { usePrevious } from './use-previous';\n\nfunction sendFirstVisit(pathname: string) {\n if (config.storage.getItem(keys.first_visit_time)) return;\n track('first_visit', {\n page_path: pathname,\n page_title: document.title,\n page_referrer: document.referrer,\n page_location: window.location.href,\n });\n config.storage.setItem(keys.first_visit_time, new Date().toISOString());\n}\n\nfunction sendUserEngagement(trigger: 'pagehide' | 'visibilitychange') {\n const engagement_time_msec = getSession().flush();\n if (engagement_time_msec <= 0) return;\n sendBeacon('user_engagement', { engagement_time_msec, trigger });\n}\n\nfunction sendScroll() {\n const engagement_time_msec = getSession().flush();\n if (engagement_time_msec <= 0) return;\n track('scroll', { engagement_time_msec });\n}\n\nfunction getScrollPercent() {\n const scrollTop = window.scrollY || document.documentElement.scrollTop;\n const windowHeight = window.innerHeight;\n const docHeight = document.documentElement.scrollHeight;\n if (docHeight === 0) return 0;\n return ((scrollTop + windowHeight) * 100) / docHeight;\n}\n\nfunction onPageHide() {\n getSession().pagehide();\n sendUserEngagement('pagehide');\n}\n\nfunction onVisibilityChange() {\n getSession().visibilitychange(document.visibilityState);\n if (document.visibilityState === 'hidden') {\n sendUserEngagement('visibilitychange');\n }\n}\n\n/**\n * 1. send session_start event when the page is loaded\n * 2. send scroll event when the user scrolls more than 90% of the page\n * 3. send user_engagement event when the page is hidden or the user is not focused\n */\nexport function useWebAnalytics(pathname: string) {\n const prevPathname = usePrevious(pathname);\n\n // reset state when the pathname changes and send scroll when the user navigates to a new page\n const hasSendScroll = useRef(false);\n useEffect(() => {\n hasSendScroll.current = false;\n }, [pathname]);\n\n useEffect(() => {\n // One lookup for the whole effect: `addEventListener` and its matching\n // `removeEventListener` have to be handed the very same function.\n const session = getSession();\n\n sendFirstVisit(pathname);\n
|
|
1
|
+
{"version":3,"file":"use-web-analytics.mjs","names":[],"sources":["../../src/hooks/use-web-analytics.ts"],"sourcesContent":["import { throttle } from '@shware/utils';\nimport { useEffect, useRef } from 'react';\nimport { keys } from '../constants/storage';\nimport { config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { sendBeacon, track } from '../track/index';\nimport { usePrevious } from './use-previous';\n\nfunction sendFirstVisit(pathname: string) {\n if (config.storage.getItem(keys.first_visit_time)) return;\n track('first_visit', {\n page_path: pathname,\n page_title: document.title,\n page_referrer: document.referrer,\n page_location: window.location.href,\n });\n config.storage.setItem(keys.first_visit_time, new Date().toISOString());\n}\n\nfunction sendUserEngagement(trigger: 'pagehide' | 'visibilitychange') {\n const engagement_time_msec = getSession().flush();\n if (engagement_time_msec <= 0) return;\n sendBeacon('user_engagement', { engagement_time_msec, trigger });\n}\n\nfunction sendScroll() {\n const engagement_time_msec = getSession().flush();\n if (engagement_time_msec <= 0) return;\n track('scroll', { engagement_time_msec });\n}\n\nfunction getScrollPercent() {\n const scrollTop = window.scrollY || document.documentElement.scrollTop;\n const windowHeight = window.innerHeight;\n const docHeight = document.documentElement.scrollHeight;\n if (docHeight === 0) return 0;\n return ((scrollTop + windowHeight) * 100) / docHeight;\n}\n\nfunction onPageHide() {\n getSession().pagehide();\n sendUserEngagement('pagehide');\n}\n\nfunction onVisibilityChange() {\n getSession().visibilitychange(document.visibilityState);\n if (document.visibilityState === 'hidden') {\n sendUserEngagement('visibilitychange');\n }\n}\n\n/**\n * 1. send session_start event when the page is loaded\n * 2. send scroll event when the user scrolls more than 90% of the page\n * 3. send user_engagement event when the page is hidden or the user is not focused\n */\nexport function useWebAnalytics(pathname: string) {\n const prevPathname = usePrevious(pathname);\n\n // reset state when the pathname changes and send scroll when the user navigates to a new page\n const hasSendScroll = useRef(false);\n useEffect(() => {\n hasSendScroll.current = false;\n }, [pathname]);\n\n useEffect(() => {\n // One lookup for the whole effect: `addEventListener` and its matching\n // `removeEventListener` have to be handed the very same function.\n const session = getSession();\n\n sendFirstVisit(pathname);\n\n const onScroll = throttle(() => {\n session.updateAccumulator();\n if (hasSendScroll.current) return;\n // only send scroll when the user has scrolled more than 90% of the page\n if (getScrollPercent() < 90) return;\n hasSendScroll.current = true;\n sendScroll();\n }, 500);\n\n const checkpointEvents = ['mousedown', 'keydown', 'touchstart'];\n const checkpoint = throttle(session.updateAccumulator, 1000);\n\n window.addEventListener('focus', session.focus);\n window.addEventListener('blur', session.blur);\n window.addEventListener('scroll', onScroll, { passive: true });\n window.addEventListener('pageshow', session.pageshow);\n window.addEventListener('pagehide', onPageHide);\n document.addEventListener('visibilitychange', onVisibilityChange);\n\n // save checkpoint\n checkpointEvents.forEach((e) => {\n window.addEventListener(e, checkpoint, { passive: true, capture: true });\n });\n\n return () => {\n window.removeEventListener('focus', session.focus);\n window.removeEventListener('blur', session.blur);\n window.removeEventListener('scroll', onScroll);\n window.removeEventListener('pageshow', session.pageshow);\n window.removeEventListener('pagehide', onPageHide);\n document.removeEventListener('visibilitychange', onVisibilityChange);\n checkpointEvents.forEach((e) => window.removeEventListener(e, checkpoint, { capture: true }));\n\n onScroll.cancel();\n checkpoint.cancel();\n };\n }, []);\n\n useEffect(() => {\n track('page_view', {\n page_path: pathname,\n page_title: document.title,\n page_referrer: document.referrer,\n page_location: window.location.href,\n previous_page_path: prevPathname ?? undefined,\n engagement_time_msec: prevPathname ? getSession().flush() : undefined,\n });\n }, [pathname]);\n}\n"],"mappings":"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"}
|
package/dist/next/index.cjs
CHANGED
|
@@ -4,11 +4,11 @@ const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
|
|
|
4
4
|
const require_track_index = require("../track/index.cjs");
|
|
5
5
|
const require_hooks_use_outbound_click_analytics = require("../hooks/use-outbound-click-analytics.cjs");
|
|
6
6
|
const require_hooks_use_web_analytics = require("../hooks/use-web-analytics.cjs");
|
|
7
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
7
8
|
let next_navigation_js = require("next/navigation.js");
|
|
8
9
|
let next_script_js = require("next/script.js");
|
|
9
10
|
next_script_js = require_runtime.__toESM(next_script_js, 1);
|
|
10
11
|
let next_web_vitals_js = require("next/web-vitals.js");
|
|
11
|
-
let react_jsx_runtime = require("react/jsx-runtime");
|
|
12
12
|
//#region src/next/index.tsx
|
|
13
13
|
function Analytics({ gaId, gaSrc, nonce, debugMode, metaPixelId, hotjarId, redditPixelId, linkedInPartnerId, facebookAppId, reportWebVitals = true }) {
|
|
14
14
|
require_hooks_use_web_analytics.useWebAnalytics((0, next_navigation_js.usePathname)());
|
package/dist/next/index.mjs
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import { track } from "../track/index.mjs";
|
|
3
3
|
import { useOutboundClickAnalytics } from "../hooks/use-outbound-click-analytics.mjs";
|
|
4
4
|
import { useWebAnalytics } from "../hooks/use-web-analytics.mjs";
|
|
5
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
6
|
import { usePathname } from "next/navigation.js";
|
|
6
7
|
import Script from "next/script.js";
|
|
7
8
|
import { useReportWebVitals } from "next/web-vitals.js";
|
|
8
|
-
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
9
|
//#region src/next/index.tsx
|
|
10
10
|
function Analytics({ gaId, gaSrc, nonce, debugMode, metaPixelId, hotjarId, redditPixelId, linkedInPartnerId, facebookAppId, reportWebVitals = true }) {
|
|
11
11
|
useWebAnalytics(usePathname());
|
|
@@ -3,8 +3,8 @@ const require_track_index = require("../track/index.cjs");
|
|
|
3
3
|
const require_hooks_use_outbound_click_analytics = require("../hooks/use-outbound-click-analytics.cjs");
|
|
4
4
|
const require_hooks_use_report_web_vitals = require("../hooks/use-report-web-vitals.cjs");
|
|
5
5
|
const require_hooks_use_web_analytics = require("../hooks/use-web-analytics.cjs");
|
|
6
|
-
let react_jsx_runtime = require("react/jsx-runtime");
|
|
7
6
|
let react_router = require("react-router");
|
|
7
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
8
8
|
//#region src/react-router/index.tsx
|
|
9
9
|
function Analytics({ gaId, gaSrc, nonce, debugMode, metaPixelId, redditPixelId, linkedInPartnerId, hotjarId, facebookAppId, reportWebVitals = true }) {
|
|
10
10
|
const { pathname } = (0, react_router.useLocation)();
|
|
@@ -2,8 +2,8 @@ import { track } from "../track/index.mjs";
|
|
|
2
2
|
import { useOutboundClickAnalytics } from "../hooks/use-outbound-click-analytics.mjs";
|
|
3
3
|
import { useReportWebVitals } from "../hooks/use-report-web-vitals.mjs";
|
|
4
4
|
import { useWebAnalytics } from "../hooks/use-web-analytics.mjs";
|
|
5
|
-
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
5
|
import { useLocation } from "react-router";
|
|
6
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
7
7
|
//#region src/react-router/index.tsx
|
|
8
8
|
function Analytics({ gaId, gaSrc, nonce, debugMode, metaPixelId, redditPixelId, linkedInPartnerId, hotjarId, facebookAppId, reportWebVitals = true }) {
|
|
9
9
|
const { pathname } = useLocation();
|
package/dist/schema/index.cjs
CHANGED
|
@@ -6,7 +6,7 @@ const MAX_VALUE_LENGTH = 512;
|
|
|
6
6
|
const MAX_PROPERTIES = 64;
|
|
7
7
|
/**
|
|
8
8
|
* Truncated rather than rejected. These schemas validate a whole batch at once, so refusing one
|
|
9
|
-
* oversized value costs every event that
|
|
9
|
+
* oversized value costs every event that traveled with it — and the values that overrun are the
|
|
10
10
|
* ones derived from the page (a link's text, a URL carrying a long query), which no client can
|
|
11
11
|
* bound in advance. A shortened value is worth more than a lost batch.
|
|
12
12
|
*/
|
|
@@ -23,11 +23,13 @@ const propertyText = (0, zod_mini.pipe)((0, zod_mini.string)(), (0, zod_mini.tra
|
|
|
23
23
|
*/
|
|
24
24
|
function takeProperties(data) {
|
|
25
25
|
const result = {};
|
|
26
|
+
let count = 0;
|
|
26
27
|
for (const [rawKey, value] of Object.entries(data)) {
|
|
28
|
+
if (count >= MAX_PROPERTIES) break;
|
|
27
29
|
const key = rawKey.trim();
|
|
28
30
|
if (!key || key.length > MAX_KEY_LENGTH) continue;
|
|
29
|
-
if (Object.keys(result).length >= MAX_PROPERTIES) break;
|
|
30
31
|
result[key] = value;
|
|
32
|
+
count++;
|
|
31
33
|
}
|
|
32
34
|
return result;
|
|
33
35
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["iso"],"sources":["../../src/schema/index.ts"],"sourcesContent":["import {\n enum as _enum,\n null as _null,\n array,\n boolean,\n e164,\n email,\n iso,\n maxLength,\n minLength,\n number,\n object,\n optional,\n pipe,\n record,\n refine,\n regex,\n string,\n toLowerCase,\n toUpperCase,\n transform,\n trim,\n union,\n url,\n uuid,\n type z,\n} from 'zod/mini';\nimport type { Environment, Platform } from '../track/types';\n\nconst MAX_KEY_LENGTH = 128;\nconst MAX_VALUE_LENGTH = 512;\nconst MAX_PROPERTIES = 64;\n\n/**\n * Truncated rather than rejected. These schemas validate a whole batch at once, so refusing one\n * oversized value costs every event that travelled with it — and the values that overrun are the\n * ones derived from the page (a link's text, a URL carrying a long query), which no client can\n * bound in advance. A shortened value is worth more than a lost batch.\n */\nconst propertyText = pipe(\n string(),\n transform((value) => value.slice(0, MAX_VALUE_LENGTH))\n);\n\n/**\n * Keys are written by hand in instrumentation code, so an unusable one is a mistake in the host\n * rather than something the visitor typed. It is still dropped rather than rejected, for the same\n * reason: the mistake should cost that property, not the batch it happens to be in. Truncating a\n * key is not an option — two long keys would silently become one field.\n *\n * The key schema below is deliberately permissive so that this transform is reached at all; the\n * trimming it used to do happens here instead. `MAX_PROPERTIES` keeps the first N in insertion\n * order.\n */\nfunction takeProperties<T>(data: Record<string, T>): Record<string, T> {\n const result: Record<string, T> = {};\n for (const [rawKey, value] of Object.entries(data)) {\n const key = rawKey.trim();\n if (!key || key.length > MAX_KEY_LENGTH) continue;\n if (Object.keys(result).length >= MAX_PROPERTIES) break;\n result[key] = value;\n }\n return result;\n}\n\nconst items = array(\n pipe(\n record(string(), union([propertyText, number(), boolean(), _null()])),\n transform(takeProperties)\n )\n);\n\nexport const ALL_PLATFORMS = [\n 'ios',\n 'android',\n 'web',\n 'macos',\n 'windows',\n 'linux',\n 'unknown',\n] as const satisfies Platform[];\n\nexport const ALL_ENVIRONMENTS = ['development', 'production'] as const satisfies Environment[];\n\nexport const tagsSchema = object({\n os: optional(string()),\n os_name: optional(string()),\n os_version: optional(string()),\n browser: optional(string()),\n browser_name: optional(string()),\n browser_version: optional(string()),\n device: optional(string()),\n device_id: optional(string().check(trim(), minLength(1), maxLength(36))),\n device_type: optional(string()),\n device_vendor: optional(string()),\n device_pixel_ratio: optional(number()),\n screen_width: optional(number()),\n screen_height: optional(number()),\n screen_resolution: optional(\n pipe(\n string().check(regex(/^\\d+x\\d+$/)),\n transform((v) => v as `${number}x${number}`)\n )\n ),\n release: optional(string()),\n language: optional(string()),\n time_zone: optional(string()),\n page_location: optional(string()),\n page_referrer: optional(string()),\n page_title: optional(string()),\n // app info\n advertising_id: optional(string()),\n install_referrer: optional(string()),\n // Meta Ads\n fbc: optional(string()),\n fbp: optional(string()),\n fbclid: optional(string()),\n ad_id: optional(string()),\n ad_name: optional(string()),\n adset_id: optional(string()),\n adset_name: optional(string()),\n campaign_id: optional(string()),\n campaign_name: optional(string()),\n placement: optional(string()),\n site_source_name: optional(string()),\n // Google Ads\n gclid: optional(string()),\n gclsrc: optional(string()),\n gad_source: optional(string()),\n gad_campaignid: optional(string()),\n // Reddit ads\n rdt_cid: optional(string()),\n rdt_uuid: optional(string()),\n // click ids\n dclid: optional(string()),\n ko_click_id: optional(string()),\n li_fat_id: optional(string()),\n msclkid: optional(string()),\n sccid: optional(string()),\n ttclid: optional(string()),\n twclid: optional(string()),\n wbraid: optional(string()),\n yclid: optional(string()),\n // utm params\n utm_source: optional(string()),\n utm_medium: optional(string()),\n utm_campaign: optional(string()),\n utm_term: optional(string()),\n utm_content: optional(string()),\n utm_id: optional(string()),\n utm_source_platform: optional(string()),\n utm_creative_format: optional(string()),\n utm_marketing_tactic: optional(string()),\n});\n\nexport const propertiesSchema = optional(\n pipe(\n record(string(), union([propertyText, number(), boolean(), _null(), items])),\n transform(takeProperties)\n )\n);\n\n/** Visitor properties differ from event properties only in taking no nested item lists. */\nconst visitorPropertiesSchema = optional(\n pipe(\n record(string(), union([propertyText, number(), boolean(), _null()])),\n transform(takeProperties)\n )\n);\n\nexport const createTrackEventSchema = array(\n object({\n name: string().check(trim(), minLength(1), maxLength(64)),\n visitor_id: uuid(),\n session_id: uuid(),\n platform: _enum(ALL_PLATFORMS),\n environment: _enum(ALL_ENVIRONMENTS),\n timestamp: iso.datetime(),\n tags: tagsSchema,\n properties: propertiesSchema,\n })\n).check(minLength(1), maxLength(100));\n\nexport const createVisitorSchema = object({\n device_id: string().check(trim(), minLength(1), maxLength(36)),\n platform: _enum(ALL_PLATFORMS),\n environment: _enum(ALL_ENVIRONMENTS),\n tags: tagsSchema,\n properties: visitorPropertiesSchema,\n});\n\nconst emailValue = pipe(string().check(trim(), toLowerCase(), maxLength(320)), email());\n\n/** E.164: a plus sign (+) prefix, country code, then digits only, no dashes/parens/spaces. */\nconst phoneValue = pipe(string().check(trim()), e164());\n\nconst addressValue = object({\n first_name: optional(string().check(trim(), maxLength(128))),\n last_name: optional(string().check(trim(), maxLength(128))),\n street: optional(string().check(trim(), maxLength(256))),\n city: optional(string().check(trim(), maxLength(128))),\n /** User province, state, or region. Example: `Hampshire` */\n region: optional(string().check(trim(), maxLength(128))),\n postal_code: optional(string().check(trim(), maxLength(32))),\n /** 2-letter country code, per the ISO 3166-1 alpha-2 standard. Example: `UK` */\n country: optional(string().check(trim(), toUpperCase(), regex(/^[A-Z]{2}$/))),\n});\n\n/**\n * User-provided data (UPD) used for enhanced conversions, Customer Match, and demographics.\n * Values are sent unhashed; Google normalizes and hashes them before they reach its servers.\n *\n * Multiple values may be sent to increase the match rate: up to 3 emails, 3 phone numbers, and\n * 2 addresses.\n *\n * @see https://support.google.com/analytics/answer/14078702\n * @see https://support.google.com/google-ads/answer/13258081\n */\nexport const userProvidedDataSchema = object({\n email: optional(union([emailValue, array(emailValue).check(minLength(1), maxLength(3))])),\n phone_number: optional(union([phoneValue, array(phoneValue).check(minLength(1), maxLength(3))])),\n address: optional(union([addressValue, array(addressValue).check(minLength(1), maxLength(2))])),\n});\n\nexport const updateVisitorSchema = object({\n user_id: optional(uuid()),\n user_data: optional(userProvidedDataSchema),\n distinct_id: optional(string().check(trim(), minLength(1), maxLength(36))),\n tags: tagsSchema,\n properties: visitorPropertiesSchema,\n});\n\nexport const createFeedbackSchema = object({\n name: string().check(minLength(1), maxLength(256)),\n email: email().check(maxLength(320)),\n message: string().check(minLength(1), maxLength(65536)),\n});\n\nconst noEmptyString = pipe(\n string().check(maxLength(256)),\n transform((v) => (v ? v : undefined))\n);\n\n/**\n * The schema for creating a link.\n * @see https://support.google.com/analytics/answer/10917952\n * */\nexport const createLinkSchema = object({\n /** The URL that the user is redirected to. */\n url: url().check(minLength(1), maxLength(1024)), // required\n\n /**\n * Campaign ID. Used to identify a specific campaign or promotion. This is a required key for GA4\n * data import. Use the same IDs that you use when uploading campaign cost data.\n */\n utm_id: optional(noEmptyString),\n\n /** Referrer, for example: google, newsletter4, billboard */\n utm_source: string().check(minLength(1), maxLength(256)), // required\n\n /** Marketing medium, for example: cpc, banner, email */\n utm_medium: string().check(minLength(1), maxLength(256)), // required\n\n /** Product, slogan, promo code, for example, spring_sale */\n utm_campaign: string().check(minLength(1), maxLength(256)), // required\n\n /** Paid keyword */\n utm_term: optional(noEmptyString),\n\n /**\n * Use to differentiate creatives. For example, if you have two call-to-action links within the\n * same email message, you can use utm_content and set different values for each so you can tell\n * which version is more effective.\n */\n utm_content: optional(noEmptyString),\n\n /**\n * The platform responsible for directing traffic to a given Analytics property (such as a buying\n * platform that sets budgets and targeting criteria or a platform that manages organic traffic\n * data). For example, Search Ads 360 or Display & Video 360.\n */\n utm_source_platform: optional(noEmptyString),\n\n /**\n * Type of creative, for example, display, native, video, search, utm_creative_format is not\n * currently reported in Google Analytics 4 properties.\n */\n utm_creative_format: optional(noEmptyString),\n\n /**\n * Targeting criteria applied to a campaign, for example, remarketing, prospecting,\n * utm_marketing_tactic is not currently reported in Google Analytics 4 properties.\n * */\n utm_marketing_tactic: optional(noEmptyString),\n});\n\nexport type CreateTrackEventDTO = z.output<typeof createTrackEventSchema>;\nexport type CreateFeedbackDTO = z.output<typeof createFeedbackSchema>;\nexport type CreateLinkDTO = z.output<typeof createLinkSchema>;\nexport type CreateVisitorDTO = z.output<typeof createVisitorSchema>;\nexport type UserProvidedDataDTO = z.output<typeof userProvidedDataSchema>;\nexport type UpdateVisitorDTO = z.output<typeof updateVisitorSchema>;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["iso"],"sources":["../../src/schema/index.ts"],"sourcesContent":["import {\n enum as _enum,\n null as _null,\n array,\n boolean,\n e164,\n email,\n iso,\n maxLength,\n minLength,\n number,\n object,\n optional,\n pipe,\n record,\n regex,\n string,\n toLowerCase,\n toUpperCase,\n transform,\n trim,\n union,\n url,\n uuid,\n type z,\n} from 'zod/mini';\nimport type { Environment, Platform } from '../track/types';\n\nconst MAX_KEY_LENGTH = 128;\nconst MAX_VALUE_LENGTH = 512;\nconst MAX_PROPERTIES = 64;\n\n/**\n * Truncated rather than rejected. These schemas validate a whole batch at once, so refusing one\n * oversized value costs every event that traveled with it — and the values that overrun are the\n * ones derived from the page (a link's text, a URL carrying a long query), which no client can\n * bound in advance. A shortened value is worth more than a lost batch.\n */\nconst propertyText = pipe(\n string(),\n transform((value) => value.slice(0, MAX_VALUE_LENGTH))\n);\n\n/**\n * Keys are written by hand in instrumentation code, so an unusable one is a mistake in the host\n * rather than something the visitor typed. It is still dropped rather than rejected, for the same\n * reason: the mistake should cost that property, not the batch it happens to be in. Truncating a\n * key is not an option — two long keys would silently become one field.\n *\n * The key schema below is deliberately permissive so that this transform is reached at all; the\n * trimming it used to do happens here instead. `MAX_PROPERTIES` keeps the first N in insertion\n * order.\n */\nfunction takeProperties<T>(data: Record<string, T>): Record<string, T> {\n const result: Record<string, T> = {};\n let count = 0;\n for (const [rawKey, value] of Object.entries(data)) {\n if (count >= MAX_PROPERTIES) break;\n const key = rawKey.trim();\n if (!key || key.length > MAX_KEY_LENGTH) continue;\n result[key] = value;\n count++;\n }\n return result;\n}\n\nconst items = array(\n pipe(\n record(string(), union([propertyText, number(), boolean(), _null()])),\n transform(takeProperties)\n )\n);\n\nexport const ALL_PLATFORMS = [\n 'ios',\n 'android',\n 'web',\n 'macos',\n 'windows',\n 'linux',\n 'unknown',\n] as const satisfies Platform[];\n\nexport const ALL_ENVIRONMENTS = ['development', 'production'] as const satisfies Environment[];\n\nexport const tagsSchema = object({\n os: optional(string()),\n os_name: optional(string()),\n os_version: optional(string()),\n browser: optional(string()),\n browser_name: optional(string()),\n browser_version: optional(string()),\n device: optional(string()),\n device_id: optional(string().check(trim(), minLength(1), maxLength(36))),\n device_type: optional(string()),\n device_vendor: optional(string()),\n device_pixel_ratio: optional(number()),\n screen_width: optional(number()),\n screen_height: optional(number()),\n screen_resolution: optional(\n pipe(\n string().check(regex(/^\\d+x\\d+$/)),\n transform((v) => v as `${number}x${number}`)\n )\n ),\n release: optional(string()),\n language: optional(string()),\n time_zone: optional(string()),\n page_location: optional(string()),\n page_referrer: optional(string()),\n page_title: optional(string()),\n // app info\n advertising_id: optional(string()),\n install_referrer: optional(string()),\n // Meta Ads\n fbc: optional(string()),\n fbp: optional(string()),\n fbclid: optional(string()),\n ad_id: optional(string()),\n ad_name: optional(string()),\n adset_id: optional(string()),\n adset_name: optional(string()),\n campaign_id: optional(string()),\n campaign_name: optional(string()),\n placement: optional(string()),\n site_source_name: optional(string()),\n // Google Ads\n gclid: optional(string()),\n gclsrc: optional(string()),\n gad_source: optional(string()),\n gad_campaignid: optional(string()),\n // Reddit ads\n rdt_cid: optional(string()),\n rdt_uuid: optional(string()),\n // click ids\n dclid: optional(string()),\n ko_click_id: optional(string()),\n li_fat_id: optional(string()),\n msclkid: optional(string()),\n sccid: optional(string()),\n ttclid: optional(string()),\n twclid: optional(string()),\n wbraid: optional(string()),\n yclid: optional(string()),\n // utm params\n utm_source: optional(string()),\n utm_medium: optional(string()),\n utm_campaign: optional(string()),\n utm_term: optional(string()),\n utm_content: optional(string()),\n utm_id: optional(string()),\n utm_source_platform: optional(string()),\n utm_creative_format: optional(string()),\n utm_marketing_tactic: optional(string()),\n});\n\nexport const propertiesSchema = optional(\n pipe(\n record(string(), union([propertyText, number(), boolean(), _null(), items])),\n transform(takeProperties)\n )\n);\n\n/** Visitor properties differ from event properties only in taking no nested item lists. */\nconst visitorPropertiesSchema = optional(\n pipe(\n record(string(), union([propertyText, number(), boolean(), _null()])),\n transform(takeProperties)\n )\n);\n\nexport const createTrackEventSchema = array(\n object({\n name: string().check(trim(), minLength(1), maxLength(64)),\n visitor_id: uuid(),\n session_id: uuid(),\n platform: _enum(ALL_PLATFORMS),\n environment: _enum(ALL_ENVIRONMENTS),\n timestamp: iso.datetime(),\n tags: tagsSchema,\n properties: propertiesSchema,\n })\n).check(minLength(1), maxLength(100));\n\nexport const createVisitorSchema = object({\n device_id: string().check(trim(), minLength(1), maxLength(36)),\n platform: _enum(ALL_PLATFORMS),\n environment: _enum(ALL_ENVIRONMENTS),\n tags: tagsSchema,\n properties: visitorPropertiesSchema,\n});\n\nconst emailValue = pipe(string().check(trim(), toLowerCase(), maxLength(320)), email());\n\n/** E.164: a plus sign (+) prefix, country code, then digits only, no dashes/parens/spaces. */\nconst phoneValue = pipe(string().check(trim()), e164());\n\nconst addressValue = object({\n first_name: optional(string().check(trim(), maxLength(128))),\n last_name: optional(string().check(trim(), maxLength(128))),\n street: optional(string().check(trim(), maxLength(256))),\n city: optional(string().check(trim(), maxLength(128))),\n /** User province, state, or region. Example: `Hampshire` */\n region: optional(string().check(trim(), maxLength(128))),\n postal_code: optional(string().check(trim(), maxLength(32))),\n /** 2-letter country code, per the ISO 3166-1 alpha-2 standard. Example: `UK` */\n country: optional(string().check(trim(), toUpperCase(), regex(/^[A-Z]{2}$/))),\n});\n\n/**\n * User-provided data (UPD) used for enhanced conversions, Customer Match, and demographics.\n * Values are sent unhashed; Google normalizes and hashes them before they reach its servers.\n *\n * Multiple values may be sent to increase the match rate: up to 3 emails, 3 phone numbers, and\n * 2 addresses.\n *\n * @see https://support.google.com/analytics/answer/14078702\n * @see https://support.google.com/google-ads/answer/13258081\n */\nexport const userProvidedDataSchema = object({\n email: optional(union([emailValue, array(emailValue).check(minLength(1), maxLength(3))])),\n phone_number: optional(union([phoneValue, array(phoneValue).check(minLength(1), maxLength(3))])),\n address: optional(union([addressValue, array(addressValue).check(minLength(1), maxLength(2))])),\n});\n\nexport const updateVisitorSchema = object({\n user_id: optional(uuid()),\n user_data: optional(userProvidedDataSchema),\n distinct_id: optional(string().check(trim(), minLength(1), maxLength(36))),\n tags: tagsSchema,\n properties: visitorPropertiesSchema,\n});\n\nexport const createFeedbackSchema = object({\n name: string().check(minLength(1), maxLength(256)),\n email: email().check(maxLength(320)),\n message: string().check(minLength(1), maxLength(65536)),\n});\n\nconst noEmptyString = pipe(\n string().check(maxLength(256)),\n transform((v) => (v ? v : undefined))\n);\n\n/**\n * The schema for creating a link.\n * @see https://support.google.com/analytics/answer/10917952\n * */\nexport const createLinkSchema = object({\n /** The URL that the user is redirected to. */\n url: url().check(minLength(1), maxLength(1024)), // required\n\n /**\n * Campaign ID. Used to identify a specific campaign or promotion. This is a required key for GA4\n * data import. Use the same IDs that you use when uploading campaign cost data.\n */\n utm_id: optional(noEmptyString),\n\n /** Referrer, for example: google, newsletter4, billboard */\n utm_source: string().check(minLength(1), maxLength(256)), // required\n\n /** Marketing medium, for example: cpc, banner, email */\n utm_medium: string().check(minLength(1), maxLength(256)), // required\n\n /** Product, slogan, promo code, for example, spring_sale */\n utm_campaign: string().check(minLength(1), maxLength(256)), // required\n\n /** Paid keyword */\n utm_term: optional(noEmptyString),\n\n /**\n * Use to differentiate creatives. For example, if you have two call-to-action links within the\n * same email message, you can use utm_content and set different values for each so you can tell\n * which version is more effective.\n */\n utm_content: optional(noEmptyString),\n\n /**\n * The platform responsible for directing traffic to a given Analytics property (such as a buying\n * platform that sets budgets and targeting criteria or a platform that manages organic traffic\n * data). For example, Search Ads 360 or Display & Video 360.\n */\n utm_source_platform: optional(noEmptyString),\n\n /**\n * Type of creative, for example, display, native, video, search, utm_creative_format is not\n * currently reported in Google Analytics 4 properties.\n */\n utm_creative_format: optional(noEmptyString),\n\n /**\n * Targeting criteria applied to a campaign, for example, remarketing, prospecting,\n * utm_marketing_tactic is not currently reported in Google Analytics 4 properties.\n * */\n utm_marketing_tactic: optional(noEmptyString),\n});\n\nexport type CreateTrackEventDTO = z.output<typeof createTrackEventSchema>;\nexport type CreateFeedbackDTO = z.output<typeof createFeedbackSchema>;\nexport type CreateLinkDTO = z.output<typeof createLinkSchema>;\nexport type CreateVisitorDTO = z.output<typeof createVisitorSchema>;\nexport type UserProvidedDataDTO = z.output<typeof userProvidedDataSchema>;\nexport type UpdateVisitorDTO = z.output<typeof updateVisitorSchema>;\n"],"mappings":";;;AA4BA,MAAM,iBAAiB;AACvB,MAAM,mBAAmB;AACzB,MAAM,iBAAiB;;;;;;;AAQvB,MAAM,gBAAA,GAAA,SAAA,KAAA,EAAA,GAAA,SAAA,OAAA,CACG,IAAA,GAAA,SAAA,UAAA,EACI,UAAU,MAAM,MAAM,GAAG,gBAAgB,CAAC,CACvD;;;;;;;;;;;AAYA,SAAS,eAAkB,MAA4C;CACrE,MAAM,SAA4B,CAAC;CACnC,IAAI,QAAQ;CACZ,KAAK,MAAM,CAAC,QAAQ,UAAU,OAAO,QAAQ,IAAI,GAAG;EAClD,IAAI,SAAS,gBAAgB;EAC7B,MAAM,MAAM,OAAO,KAAK;EACxB,IAAI,CAAC,OAAO,IAAI,SAAS,gBAAgB;EACzC,OAAO,OAAO;EACd;CACF;CACA,OAAO;AACT;AAEA,MAAM,SAAA,GAAA,SAAA,MAAA,EAAA,GAAA,SAAA,KAAA,EAAA,GAAA,SAAA,OAAA,EAAA,GAAA,SAAA,OAAA,CAEY,IAAA,GAAA,SAAA,MAAA,CAAS;CAAC;EAAqB,GAAA,SAAA,OAAA,CAAA;EAAW,GAAA,SAAA,QAAA,CAAA;EAAS,GAAA,SAAA,KAAA,CAAA;AAAC,CAAC,CAAC,IAAA,GAAA,SAAA,UAAA,CAC1D,cAAc,CAC1B,CACF;AAEA,MAAa,gBAAgB;CAC3B;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,MAAa,mBAAmB,CAAC,eAAe,YAAY;AAE5D,MAAa,cAAA,GAAA,SAAA,OAAA,CAAoB;CAC/B,KAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAoB,CAAC;CACrB,UAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAyB,CAAC;CAC1B,aAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAA4B,CAAC;CAC7B,UAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAyB,CAAC;CAC1B,eAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAA8B,CAAC;CAC/B,kBAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAiC,CAAC;CAClC,SAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAwB,CAAC;CACzB,YAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAA2B,CAAC,CAAC,OAAA,GAAA,SAAA,KAAA,CAAW,IAAA,GAAA,SAAA,UAAA,CAAa,CAAC,IAAA,GAAA,SAAA,UAAA,CAAa,EAAE,CAAC,CAAC;CACvE,cAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAA6B,CAAC;CAC9B,gBAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAA+B,CAAC;CAChC,qBAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAoC,CAAC;CACrC,eAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAA8B,CAAC;CAC/B,gBAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAA+B,CAAC;CAChC,oBAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,KAAA,EAAA,GAAA,SAAA,OAAA,CAEW,CAAC,CAAC,OAAA,GAAA,SAAA,MAAA,CAAY,WAAW,CAAC,IAAA,GAAA,SAAA,UAAA,EACtB,MAAM,CAA0B,CAC7C,CACF;CACA,UAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAyB,CAAC;CAC1B,WAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAA0B,CAAC;CAC3B,YAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAA2B,CAAC;CAC5B,gBAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAA+B,CAAC;CAChC,gBAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAA+B,CAAC;CAChC,aAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAA4B,CAAC;CAE7B,iBAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAgC,CAAC;CACjC,mBAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAkC,CAAC;CAEnC,MAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAqB,CAAC;CACtB,MAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAqB,CAAC;CACtB,SAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAwB,CAAC;CACzB,QAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAuB,CAAC;CACxB,UAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAyB,CAAC;CAC1B,WAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAA0B,CAAC;CAC3B,aAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAA4B,CAAC;CAC7B,cAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAA6B,CAAC;CAC9B,gBAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAA+B,CAAC;CAChC,YAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAA2B,CAAC;CAC5B,mBAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAkC,CAAC;CAEnC,QAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAuB,CAAC;CACxB,SAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAwB,CAAC;CACzB,aAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAA4B,CAAC;CAC7B,iBAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAgC,CAAC;CAEjC,UAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAyB,CAAC;CAC1B,WAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAA0B,CAAC;CAE3B,QAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAuB,CAAC;CACxB,cAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAA6B,CAAC;CAC9B,YAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAA2B,CAAC;CAC5B,UAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAyB,CAAC;CAC1B,QAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAuB,CAAC;CACxB,SAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAwB,CAAC;CACzB,SAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAwB,CAAC;CACzB,SAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAwB,CAAC;CACzB,QAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAuB,CAAC;CAExB,aAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAA4B,CAAC;CAC7B,aAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAA4B,CAAC;CAC7B,eAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAA8B,CAAC;CAC/B,WAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAA0B,CAAC;CAC3B,cAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAA6B,CAAC;CAC9B,SAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAwB,CAAC;CACzB,sBAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAqC,CAAC;CACtC,sBAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAqC,CAAC;CACtC,uBAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAsC,CAAC;AACzC,CAAC;AAED,MAAa,oBAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,KAAA,EAAA,GAAA,SAAA,OAAA,EAAA,GAAA,SAAA,OAAA,CAEK,IAAA,GAAA,SAAA,MAAA,CAAS;CAAC;EAAqB,GAAA,SAAA,OAAA,CAAA;EAAW,GAAA,SAAA,QAAA,CAAA;EAAS,GAAA,SAAA,KAAA,CAAA;CAAG;AAAK,CAAC,CAAC,IAAA,GAAA,SAAA,UAAA,CACjE,cAAc,CAC1B,CACF;;AAGA,MAAM,2BAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,KAAA,EAAA,GAAA,SAAA,OAAA,EAAA,GAAA,SAAA,OAAA,CAEY,IAAA,GAAA,SAAA,MAAA,CAAS;CAAC;EAAqB,GAAA,SAAA,OAAA,CAAA;EAAW,GAAA,SAAA,QAAA,CAAA;EAAS,GAAA,SAAA,KAAA,CAAA;AAAC,CAAC,CAAC,IAAA,GAAA,SAAA,UAAA,CAC1D,cAAc,CAC1B,CACF;AAEA,MAAa,0BAAA,GAAA,SAAA,MAAA,EAAA,GAAA,SAAA,OAAA,CACJ;CACL,OAAA,GAAA,SAAA,OAAA,CAAa,CAAC,CAAC,OAAA,GAAA,SAAA,KAAA,CAAW,IAAA,GAAA,SAAA,UAAA,CAAa,CAAC,IAAA,GAAA,SAAA,UAAA,CAAa,EAAE,CAAC;CACxD,aAAA,GAAA,SAAA,KAAA,CAAiB;CACjB,aAAA,GAAA,SAAA,KAAA,CAAiB;CACjB,WAAA,GAAA,SAAA,KAAA,CAAgB,aAAa;CAC7B,cAAA,GAAA,SAAA,KAAA,CAAmB,gBAAgB;CACnC,WAAWA,SAAAA,IAAI,SAAS;CACxB,MAAM;CACN,YAAY;AACd,CAAC,CACH,CAAC,CAAC,OAAA,GAAA,SAAA,UAAA,CAAgB,CAAC,IAAA,GAAA,SAAA,UAAA,CAAa,GAAG,CAAC;AAEpC,MAAa,uBAAA,GAAA,SAAA,OAAA,CAA6B;CACxC,YAAA,GAAA,SAAA,OAAA,CAAkB,CAAC,CAAC,OAAA,GAAA,SAAA,KAAA,CAAW,IAAA,GAAA,SAAA,UAAA,CAAa,CAAC,IAAA,GAAA,SAAA,UAAA,CAAa,EAAE,CAAC;CAC7D,WAAA,GAAA,SAAA,KAAA,CAAgB,aAAa;CAC7B,cAAA,GAAA,SAAA,KAAA,CAAmB,gBAAgB;CACnC,MAAM;CACN,YAAY;AACd,CAAC;AAED,MAAM,cAAA,GAAA,SAAA,KAAA,EAAA,GAAA,SAAA,OAAA,CAAyB,CAAC,CAAC,OAAA,GAAA,SAAA,KAAA,CAAW,IAAA,GAAA,SAAA,YAAA,CAAe,IAAA,GAAA,SAAA,UAAA,CAAa,GAAG,CAAC,IAAA,GAAA,SAAA,MAAA,CAAS,CAAC;;AAGtF,MAAM,cAAA,GAAA,SAAA,KAAA,EAAA,GAAA,SAAA,OAAA,CAAyB,CAAC,CAAC,OAAA,GAAA,SAAA,KAAA,CAAW,CAAC,IAAA,GAAA,SAAA,KAAA,CAAQ,CAAC;AAEtD,MAAM,gBAAA,GAAA,SAAA,OAAA,CAAsB;CAC1B,aAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAA4B,CAAC,CAAC,OAAA,GAAA,SAAA,KAAA,CAAW,IAAA,GAAA,SAAA,UAAA,CAAa,GAAG,CAAC,CAAC;CAC3D,YAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAA2B,CAAC,CAAC,OAAA,GAAA,SAAA,KAAA,CAAW,IAAA,GAAA,SAAA,UAAA,CAAa,GAAG,CAAC,CAAC;CAC1D,SAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAwB,CAAC,CAAC,OAAA,GAAA,SAAA,KAAA,CAAW,IAAA,GAAA,SAAA,UAAA,CAAa,GAAG,CAAC,CAAC;CACvD,OAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAsB,CAAC,CAAC,OAAA,GAAA,SAAA,KAAA,CAAW,IAAA,GAAA,SAAA,UAAA,CAAa,GAAG,CAAC,CAAC;;CAErD,SAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAwB,CAAC,CAAC,OAAA,GAAA,SAAA,KAAA,CAAW,IAAA,GAAA,SAAA,UAAA,CAAa,GAAG,CAAC,CAAC;CACvD,cAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAA6B,CAAC,CAAC,OAAA,GAAA,SAAA,KAAA,CAAW,IAAA,GAAA,SAAA,UAAA,CAAa,EAAE,CAAC,CAAC;;CAE3D,UAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAAyB,CAAC,CAAC,OAAA,GAAA,SAAA,KAAA,CAAW,IAAA,GAAA,SAAA,YAAA,CAAe,IAAA,GAAA,SAAA,MAAA,CAAS,YAAY,CAAC,CAAC;AAC9E,CAAC;;;;;;;;;;;AAYD,MAAa,0BAAA,GAAA,SAAA,OAAA,CAAgC;CAC3C,QAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,MAAA,CAAsB,CAAC,aAAA,GAAA,SAAA,MAAA,CAAkB,UAAU,CAAC,CAAC,OAAA,GAAA,SAAA,UAAA,CAAgB,CAAC,IAAA,GAAA,SAAA,UAAA,CAAa,CAAC,CAAC,CAAC,CAAC,CAAC;CACxF,eAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,MAAA,CAA6B,CAAC,aAAA,GAAA,SAAA,MAAA,CAAkB,UAAU,CAAC,CAAC,OAAA,GAAA,SAAA,UAAA,CAAgB,CAAC,IAAA,GAAA,SAAA,UAAA,CAAa,CAAC,CAAC,CAAC,CAAC,CAAC;CAC/F,UAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,MAAA,CAAwB,CAAC,eAAA,GAAA,SAAA,MAAA,CAAoB,YAAY,CAAC,CAAC,OAAA,GAAA,SAAA,UAAA,CAAgB,CAAC,IAAA,GAAA,SAAA,UAAA,CAAa,CAAC,CAAC,CAAC,CAAC,CAAC;AAChG,CAAC;AAED,MAAa,uBAAA,GAAA,SAAA,OAAA,CAA6B;CACxC,UAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,KAAA,CAAuB,CAAC;CACxB,YAAA,GAAA,SAAA,SAAA,CAAoB,sBAAsB;CAC1C,cAAA,GAAA,SAAA,SAAA,EAAA,GAAA,SAAA,OAAA,CAA6B,CAAC,CAAC,OAAA,GAAA,SAAA,KAAA,CAAW,IAAA,GAAA,SAAA,UAAA,CAAa,CAAC,IAAA,GAAA,SAAA,UAAA,CAAa,EAAE,CAAC,CAAC;CACzE,MAAM;CACN,YAAY;AACd,CAAC;AAED,MAAa,wBAAA,GAAA,SAAA,OAAA,CAA8B;CACzC,OAAA,GAAA,SAAA,OAAA,CAAa,CAAC,CAAC,OAAA,GAAA,SAAA,UAAA,CAAgB,CAAC,IAAA,GAAA,SAAA,UAAA,CAAa,GAAG,CAAC;CACjD,QAAA,GAAA,SAAA,MAAA,CAAa,CAAC,CAAC,OAAA,GAAA,SAAA,UAAA,CAAgB,GAAG,CAAC;CACnC,UAAA,GAAA,SAAA,OAAA,CAAgB,CAAC,CAAC,OAAA,GAAA,SAAA,UAAA,CAAgB,CAAC,IAAA,GAAA,SAAA,UAAA,CAAa,KAAK,CAAC;AACxD,CAAC;AAED,MAAM,iBAAA,GAAA,SAAA,KAAA,EAAA,GAAA,SAAA,OAAA,CACG,CAAC,CAAC,OAAA,GAAA,SAAA,UAAA,CAAgB,GAAG,CAAC,IAAA,GAAA,SAAA,UAAA,EAClB,MAAO,IAAI,IAAI,KAAA,CAAU,CACtC;;;;;AAMA,MAAa,oBAAA,GAAA,SAAA,OAAA,CAA0B;;CAErC,MAAA,GAAA,SAAA,IAAA,CAAS,CAAC,CAAC,OAAA,GAAA,SAAA,UAAA,CAAgB,CAAC,IAAA,GAAA,SAAA,UAAA,CAAa,IAAI,CAAC;;;;;CAM9C,SAAA,GAAA,SAAA,SAAA,CAAiB,aAAa;;CAG9B,aAAA,GAAA,SAAA,OAAA,CAAmB,CAAC,CAAC,OAAA,GAAA,SAAA,UAAA,CAAgB,CAAC,IAAA,GAAA,SAAA,UAAA,CAAa,GAAG,CAAC;;CAGvD,aAAA,GAAA,SAAA,OAAA,CAAmB,CAAC,CAAC,OAAA,GAAA,SAAA,UAAA,CAAgB,CAAC,IAAA,GAAA,SAAA,UAAA,CAAa,GAAG,CAAC;;CAGvD,eAAA,GAAA,SAAA,OAAA,CAAqB,CAAC,CAAC,OAAA,GAAA,SAAA,UAAA,CAAgB,CAAC,IAAA,GAAA,SAAA,UAAA,CAAa,GAAG,CAAC;;CAGzD,WAAA,GAAA,SAAA,SAAA,CAAmB,aAAa;;;;;;CAOhC,cAAA,GAAA,SAAA,SAAA,CAAsB,aAAa;;;;;;CAOnC,sBAAA,GAAA,SAAA,SAAA,CAA8B,aAAa;;;;;CAM3C,sBAAA,GAAA,SAAA,SAAA,CAA8B,aAAa;;;;;CAM3C,uBAAA,GAAA,SAAA,SAAA,CAA+B,aAAa;AAC9C,CAAC"}
|