@shware/analytics 4.2.0 → 5.1.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 +1 -1
- package/dist/click-id/index.cjs.map +1 -1
- package/dist/click-id/index.mjs +1 -1
- package/dist/click-id/index.mjs.map +1 -1
- package/dist/hooks/use-track-impression.cjs +5 -4
- package/dist/hooks/use-track-impression.cjs.map +1 -1
- package/dist/hooks/use-track-impression.d.cts +2 -1
- package/dist/hooks/use-track-impression.d.cts.map +1 -1
- package/dist/hooks/use-track-impression.d.mts +2 -1
- package/dist/hooks/use-track-impression.d.mts.map +1 -1
- package/dist/hooks/use-track-impression.mjs +6 -5
- package/dist/hooks/use-track-impression.mjs.map +1 -1
- package/dist/link/index.cjs +1 -1
- package/dist/link/index.cjs.map +1 -1
- package/dist/link/index.mjs +1 -1
- package/dist/link/index.mjs.map +1 -1
- package/dist/visitor/index.cjs +4 -2
- package/dist/visitor/index.cjs.map +1 -1
- package/dist/visitor/index.d.cts.map +1 -1
- package/dist/visitor/index.d.mts.map +1 -1
- package/dist/visitor/index.mjs +4 -2
- package/dist/visitor/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/click-id/index.cjs
CHANGED
|
@@ -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 >= 0 ? 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,KAAK,IAAI,IAAI,MAAM,IAAI,CAAC,IAAI,EAAE;CAC3D;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. `.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"}
|
package/dist/click-id/index.mjs
CHANGED
|
@@ -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 >= 0 ? 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,KAAK,IAAI,IAAI,MAAM,IAAI,CAAC,IAAI,EAAE;CAC3D;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. `.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"}
|
|
@@ -4,22 +4,23 @@ let react = require("react");
|
|
|
4
4
|
//#region src/hooks/use-track-impression.ts
|
|
5
5
|
function useTrackImpression(name, properties) {
|
|
6
6
|
const fired = (0, react.useRef)(false);
|
|
7
|
-
const
|
|
7
|
+
const [node, setNode] = (0, react.useState)(null);
|
|
8
8
|
const onTrack = (0, react.useEffectEvent)(() => {
|
|
9
9
|
if (fired.current) return;
|
|
10
10
|
require_track_index.track(name, properties);
|
|
11
11
|
fired.current = true;
|
|
12
12
|
});
|
|
13
13
|
(0, react.useEffect)(() => {
|
|
14
|
+
if (!node) return;
|
|
14
15
|
const observer = new IntersectionObserver(([entry]) => {
|
|
15
16
|
if (!entry.isIntersecting) return;
|
|
16
17
|
onTrack();
|
|
17
18
|
observer.disconnect();
|
|
18
19
|
}, { threshold: .5 });
|
|
19
|
-
|
|
20
|
+
observer.observe(node);
|
|
20
21
|
return () => observer.disconnect();
|
|
21
|
-
}, [
|
|
22
|
-
return
|
|
22
|
+
}, [node]);
|
|
23
|
+
return setNode;
|
|
23
24
|
}
|
|
24
25
|
//#endregion
|
|
25
26
|
exports.useTrackImpression = useTrackImpression;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-track-impression.cjs","names":[],"sources":["../../src/hooks/use-track-impression.ts"],"sourcesContent":["import { useEffect, useEffectEvent, useRef } from 'react';\nimport { track } from '../track/index';\nimport type { EventName, TrackName, TrackProperties } from '../track/types';\n\nexport function useTrackImpression
|
|
1
|
+
{"version":3,"file":"use-track-impression.cjs","names":[],"sources":["../../src/hooks/use-track-impression.ts"],"sourcesContent":["import { type RefCallback, useEffect, useEffectEvent, useRef, useState } from 'react';\nimport { track } from '../track/index';\nimport type { EventName, TrackName, TrackProperties } from '../track/types';\n\nexport function useTrackImpression<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n): RefCallback<Element> {\n const fired = useRef(false);\n const [node, setNode] = useState<Element | null>(null);\n\n const onTrack = useEffectEvent(() => {\n if (fired.current) return;\n track(name, properties);\n fired.current = true;\n });\n\n useEffect(() => {\n if (!node) return;\n\n const observer = new IntersectionObserver(\n ([entry]) => {\n if (!entry.isIntersecting) return;\n onTrack();\n observer.disconnect();\n },\n { threshold: 0.5 }\n );\n\n observer.observe(node);\n return () => observer.disconnect();\n }, [node]);\n\n // A callback ref (contravariant in the element type) instead of a RefObject:\n // it attaches to any element without an R type parameter, so `name` stays the\n // only inference site and explicit type arguments are never needed. It also\n // observes elements that mount late, which the previous [ref.current] effect\n // dependency missed.\n return setNode;\n}\n"],"mappings":";;;;AAIA,SAAgB,mBACd,MACA,YACsB;CACtB,MAAM,SAAA,GAAA,MAAA,OAAA,CAAe,KAAK;CAC1B,MAAM,CAAC,MAAM,YAAA,GAAA,MAAA,SAAA,CAAoC,IAAI;CAErD,MAAM,WAAA,GAAA,MAAA,eAAA,OAA+B;EACnC,IAAI,MAAM,SAAS;EACnB,oBAAA,MAAM,MAAM,UAAU;EACtB,MAAM,UAAU;CAClB,CAAC;CAED,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,IAAI,CAAC,MAAM;EAEX,MAAM,WAAW,IAAI,sBAClB,CAAC,WAAW;GACX,IAAI,CAAC,MAAM,gBAAgB;GAC3B,QAAQ;GACR,SAAS,WAAW;EACtB,GACA,EAAE,WAAW,GAAI,CACnB;EAEA,SAAS,QAAQ,IAAI;EACrB,aAAa,SAAS,WAAW;CACnC,GAAG,CAAC,IAAI,CAAC;CAOT,OAAO;AACT"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EventName, TrackName, TrackProperties } from "../track/types.cjs";
|
|
2
|
+
import { RefCallback } from "react";
|
|
2
3
|
//#region src/hooks/use-track-impression.d.ts
|
|
3
|
-
declare function useTrackImpression<
|
|
4
|
+
declare function useTrackImpression<T extends EventName = EventName>(name: TrackName<T>, properties?: TrackProperties<T>): RefCallback<Element>;
|
|
4
5
|
//#endregion
|
|
5
6
|
export { useTrackImpression };
|
|
6
7
|
//# sourceMappingURL=use-track-impression.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-track-impression.d.cts","names":[],"sources":["../../src/hooks/use-track-impression.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"use-track-impression.d.cts","names":[],"sources":["../../src/hooks/use-track-impression.ts"],"mappings":";;;iBAIgB,mBAAmB,UAAU,YAAY,WACvD,MAAM,UAAU,IAChB,aAAa,gBAAgB,KAC5B,YAAY"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EventName, TrackName, TrackProperties } from "../track/types.mjs";
|
|
2
|
+
import { RefCallback } from "react";
|
|
2
3
|
//#region src/hooks/use-track-impression.d.ts
|
|
3
|
-
declare function useTrackImpression<
|
|
4
|
+
declare function useTrackImpression<T extends EventName = EventName>(name: TrackName<T>, properties?: TrackProperties<T>): RefCallback<Element>;
|
|
4
5
|
//#endregion
|
|
5
6
|
export { useTrackImpression };
|
|
6
7
|
//# sourceMappingURL=use-track-impression.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-track-impression.d.mts","names":[],"sources":["../../src/hooks/use-track-impression.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"use-track-impression.d.mts","names":[],"sources":["../../src/hooks/use-track-impression.ts"],"mappings":";;;iBAIgB,mBAAmB,UAAU,YAAY,WACvD,MAAM,UAAU,IAChB,aAAa,gBAAgB,KAC5B,YAAY"}
|
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
import { track } from "../track/index.mjs";
|
|
2
|
-
import { useEffect, useEffectEvent, useRef } from "react";
|
|
2
|
+
import { useEffect, useEffectEvent, useRef, useState } from "react";
|
|
3
3
|
//#region src/hooks/use-track-impression.ts
|
|
4
4
|
function useTrackImpression(name, properties) {
|
|
5
5
|
const fired = useRef(false);
|
|
6
|
-
const
|
|
6
|
+
const [node, setNode] = useState(null);
|
|
7
7
|
const onTrack = useEffectEvent(() => {
|
|
8
8
|
if (fired.current) return;
|
|
9
9
|
track(name, properties);
|
|
10
10
|
fired.current = true;
|
|
11
11
|
});
|
|
12
12
|
useEffect(() => {
|
|
13
|
+
if (!node) return;
|
|
13
14
|
const observer = new IntersectionObserver(([entry]) => {
|
|
14
15
|
if (!entry.isIntersecting) return;
|
|
15
16
|
onTrack();
|
|
16
17
|
observer.disconnect();
|
|
17
18
|
}, { threshold: .5 });
|
|
18
|
-
|
|
19
|
+
observer.observe(node);
|
|
19
20
|
return () => observer.disconnect();
|
|
20
|
-
}, [
|
|
21
|
-
return
|
|
21
|
+
}, [node]);
|
|
22
|
+
return setNode;
|
|
22
23
|
}
|
|
23
24
|
//#endregion
|
|
24
25
|
export { useTrackImpression };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-track-impression.mjs","names":[],"sources":["../../src/hooks/use-track-impression.ts"],"sourcesContent":["import { useEffect, useEffectEvent, useRef } from 'react';\nimport { track } from '../track/index';\nimport type { EventName, TrackName, TrackProperties } from '../track/types';\n\nexport function useTrackImpression
|
|
1
|
+
{"version":3,"file":"use-track-impression.mjs","names":[],"sources":["../../src/hooks/use-track-impression.ts"],"sourcesContent":["import { type RefCallback, useEffect, useEffectEvent, useRef, useState } from 'react';\nimport { track } from '../track/index';\nimport type { EventName, TrackName, TrackProperties } from '../track/types';\n\nexport function useTrackImpression<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n): RefCallback<Element> {\n const fired = useRef(false);\n const [node, setNode] = useState<Element | null>(null);\n\n const onTrack = useEffectEvent(() => {\n if (fired.current) return;\n track(name, properties);\n fired.current = true;\n });\n\n useEffect(() => {\n if (!node) return;\n\n const observer = new IntersectionObserver(\n ([entry]) => {\n if (!entry.isIntersecting) return;\n onTrack();\n observer.disconnect();\n },\n { threshold: 0.5 }\n );\n\n observer.observe(node);\n return () => observer.disconnect();\n }, [node]);\n\n // A callback ref (contravariant in the element type) instead of a RefObject:\n // it attaches to any element without an R type parameter, so `name` stays the\n // only inference site and explicit type arguments are never needed. It also\n // observes elements that mount late, which the previous [ref.current] effect\n // dependency missed.\n return setNode;\n}\n"],"mappings":";;;AAIA,SAAgB,mBACd,MACA,YACsB;CACtB,MAAM,QAAQ,OAAO,KAAK;CAC1B,MAAM,CAAC,MAAM,WAAW,SAAyB,IAAI;CAErD,MAAM,UAAU,qBAAqB;EACnC,IAAI,MAAM,SAAS;EACnB,MAAM,MAAM,UAAU;EACtB,MAAM,UAAU;CAClB,CAAC;CAED,gBAAgB;EACd,IAAI,CAAC,MAAM;EAEX,MAAM,WAAW,IAAI,sBAClB,CAAC,WAAW;GACX,IAAI,CAAC,MAAM,gBAAgB;GAC3B,QAAQ;GACR,SAAS,WAAW;EACtB,GACA,EAAE,WAAW,GAAI,CACnB;EAEA,SAAS,QAAQ,IAAI;EACrB,aAAa,SAAS,WAAW;CACnC,GAAG,CAAC,IAAI,CAAC;CAOT,OAAO;AACT"}
|
package/dist/link/index.cjs
CHANGED
|
@@ -23,7 +23,7 @@ async function getLink(id) {
|
|
|
23
23
|
console.error(`Failed to get link(${id}): ${response.status} ${await response.text()}`);
|
|
24
24
|
return null;
|
|
25
25
|
}
|
|
26
|
-
return response.json();
|
|
26
|
+
return await response.json();
|
|
27
27
|
} catch {
|
|
28
28
|
console.error(`Failed to get link(${id}): network error`);
|
|
29
29
|
return null;
|
package/dist/link/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["config"],"sources":["../../src/link/index.ts"],"sourcesContent":["import { fetch } from '@shware/utils';\nimport type { CreateLinkDTO } from '../schema/index';\nimport { config } from '../setup/index';\n\nexport interface Link extends CreateLinkDTO {\n id: string;\n created_at: string;\n}\n\nexport async function createLink(dto: CreateLinkDTO) {\n const response = await fetch(`${config.endpoint}/links`, {\n method: 'POST',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to create link: ${response.status} ${await response.text()}`);\n }\n return response.json() as Promise<Link>;\n}\n\nexport async function getLink(id: string): Promise<Link | null> {\n try {\n const response = await fetch(`${config.endpoint}/links/${id}`, {\n method: 'GET',\n credentials: 'include',\n headers: await config.getHeaders(),\n });\n\n if (!response.ok) {\n console.error(`Failed to get link(${id}): ${response.status} ${await response.text()}`);\n return null;\n }\n return response.json() as
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["config"],"sources":["../../src/link/index.ts"],"sourcesContent":["import { fetch } from '@shware/utils';\nimport type { CreateLinkDTO } from '../schema/index';\nimport { config } from '../setup/index';\n\nexport interface Link extends CreateLinkDTO {\n id: string;\n created_at: string;\n}\n\nexport async function createLink(dto: CreateLinkDTO) {\n const response = await fetch(`${config.endpoint}/links`, {\n method: 'POST',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to create link: ${response.status} ${await response.text()}`);\n }\n return response.json() as Promise<Link>;\n}\n\nexport async function getLink(id: string): Promise<Link | null> {\n try {\n const response = await fetch(`${config.endpoint}/links/${id}`, {\n method: 'GET',\n credentials: 'include',\n headers: await config.getHeaders(),\n });\n\n if (!response.ok) {\n console.error(`Failed to get link(${id}): ${response.status} ${await response.text()}`);\n return null;\n }\n return (await response.json()) as Link;\n } catch {\n console.error(`Failed to get link(${id}): network error`);\n return null;\n }\n}\n"],"mappings":";;;;AASA,eAAsB,WAAW,KAAoB;CACnD,MAAM,WAAW,OAAA,GAAA,cAAA,MAAA,CAAY,GAAGA,oBAAAA,OAAO,SAAS,SAAS;EACvD,QAAQ;EACR,aAAa;EACb,SAAS,MAAMA,oBAAAA,OAAO,WAAW;EACjC,MAAM,KAAK,UAAU,GAAG;CAC1B,CAAC;CAED,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MAAM,0BAA0B,SAAS,OAAO,GAAG,MAAM,SAAS,KAAK,GAAG;CAEtF,OAAO,SAAS,KAAK;AACvB;AAEA,eAAsB,QAAQ,IAAkC;CAC9D,IAAI;EACF,MAAM,WAAW,OAAA,GAAA,cAAA,MAAA,CAAY,GAAGA,oBAAAA,OAAO,SAAS,SAAS,MAAM;GAC7D,QAAQ;GACR,aAAa;GACb,SAAS,MAAMA,oBAAAA,OAAO,WAAW;EACnC,CAAC;EAED,IAAI,CAAC,SAAS,IAAI;GAChB,QAAQ,MAAM,sBAAsB,GAAG,KAAK,SAAS,OAAO,GAAG,MAAM,SAAS,KAAK,GAAG;GACtF,OAAO;EACT;EACA,OAAQ,MAAM,SAAS,KAAK;CAC9B,QAAQ;EACN,QAAQ,MAAM,sBAAsB,GAAG,iBAAiB;EACxD,OAAO;CACT;AACF"}
|
package/dist/link/index.mjs
CHANGED
|
@@ -22,7 +22,7 @@ async function getLink(id) {
|
|
|
22
22
|
console.error(`Failed to get link(${id}): ${response.status} ${await response.text()}`);
|
|
23
23
|
return null;
|
|
24
24
|
}
|
|
25
|
-
return response.json();
|
|
25
|
+
return await response.json();
|
|
26
26
|
} catch {
|
|
27
27
|
console.error(`Failed to get link(${id}): network error`);
|
|
28
28
|
return null;
|
package/dist/link/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/link/index.ts"],"sourcesContent":["import { fetch } from '@shware/utils';\nimport type { CreateLinkDTO } from '../schema/index';\nimport { config } from '../setup/index';\n\nexport interface Link extends CreateLinkDTO {\n id: string;\n created_at: string;\n}\n\nexport async function createLink(dto: CreateLinkDTO) {\n const response = await fetch(`${config.endpoint}/links`, {\n method: 'POST',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to create link: ${response.status} ${await response.text()}`);\n }\n return response.json() as Promise<Link>;\n}\n\nexport async function getLink(id: string): Promise<Link | null> {\n try {\n const response = await fetch(`${config.endpoint}/links/${id}`, {\n method: 'GET',\n credentials: 'include',\n headers: await config.getHeaders(),\n });\n\n if (!response.ok) {\n console.error(`Failed to get link(${id}): ${response.status} ${await response.text()}`);\n return null;\n }\n return response.json() as
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/link/index.ts"],"sourcesContent":["import { fetch } from '@shware/utils';\nimport type { CreateLinkDTO } from '../schema/index';\nimport { config } from '../setup/index';\n\nexport interface Link extends CreateLinkDTO {\n id: string;\n created_at: string;\n}\n\nexport async function createLink(dto: CreateLinkDTO) {\n const response = await fetch(`${config.endpoint}/links`, {\n method: 'POST',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to create link: ${response.status} ${await response.text()}`);\n }\n return response.json() as Promise<Link>;\n}\n\nexport async function getLink(id: string): Promise<Link | null> {\n try {\n const response = await fetch(`${config.endpoint}/links/${id}`, {\n method: 'GET',\n credentials: 'include',\n headers: await config.getHeaders(),\n });\n\n if (!response.ok) {\n console.error(`Failed to get link(${id}): ${response.status} ${await response.text()}`);\n return null;\n }\n return (await response.json()) as Link;\n } catch {\n console.error(`Failed to get link(${id}): network error`);\n return null;\n }\n}\n"],"mappings":";;;AASA,eAAsB,WAAW,KAAoB;CACnD,MAAM,WAAW,MAAM,MAAM,GAAG,OAAO,SAAS,SAAS;EACvD,QAAQ;EACR,aAAa;EACb,SAAS,MAAM,OAAO,WAAW;EACjC,MAAM,KAAK,UAAU,GAAG;CAC1B,CAAC;CAED,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MAAM,0BAA0B,SAAS,OAAO,GAAG,MAAM,SAAS,KAAK,GAAG;CAEtF,OAAO,SAAS,KAAK;AACvB;AAEA,eAAsB,QAAQ,IAAkC;CAC9D,IAAI;EACF,MAAM,WAAW,MAAM,MAAM,GAAG,OAAO,SAAS,SAAS,MAAM;GAC7D,QAAQ;GACR,aAAa;GACb,SAAS,MAAM,OAAO,WAAW;EACnC,CAAC;EAED,IAAI,CAAC,SAAS,IAAI;GAChB,QAAQ,MAAM,sBAAsB,GAAG,KAAK,SAAS,OAAO,GAAG,MAAM,SAAS,KAAK,GAAG;GACtF,OAAO;EACT;EACA,OAAQ,MAAM,SAAS,KAAK;CAC9B,QAAQ;EACN,QAAQ,MAAM,sBAAsB,GAAG,iBAAiB;EACxD,OAAO;CACT;AACF"}
|
package/dist/visitor/index.cjs
CHANGED
|
@@ -24,10 +24,12 @@ async function createVisitor() {
|
|
|
24
24
|
async function getOrCreateVisitor() {
|
|
25
25
|
const visitorId = require_setup_index.config.storage.getItem(require_constants_storage.keys.visitor_id);
|
|
26
26
|
if (visitorId && visitorId !== "undefined") {
|
|
27
|
+
const tags = await require_setup_index.config.getTags();
|
|
27
28
|
const response = await (0, _shware_utils.fetch)(`${require_setup_index.config.endpoint}/visitors/${visitorId}`, {
|
|
28
|
-
method: "
|
|
29
|
+
method: "PATCH",
|
|
29
30
|
credentials: "include",
|
|
30
|
-
headers: await require_setup_index.config.getHeaders()
|
|
31
|
+
headers: await require_setup_index.config.getHeaders(),
|
|
32
|
+
body: JSON.stringify({ tags })
|
|
31
33
|
});
|
|
32
34
|
if (!response.ok) return createVisitor();
|
|
33
35
|
const data = await response.json();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["config","keys","cache"],"sources":["../../src/visitor/index.ts"],"sourcesContent":["import { fetch } from '@shware/utils';\nimport { keys } from '../constants/storage';\nimport type { CreateVisitorDTO, UpdateVisitorDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport type { Visitor, VisitorProperties } from './types';\n\nasync function createVisitor(): Promise<Visitor> {\n const tags = await config.getTags();\n const dto: CreateVisitorDTO = {\n device_id: await config.getDeviceId(),\n platform: config.platform,\n environment: config.environment,\n tags,\n properties: tags as VisitorProperties,\n };\n\n const response = await fetch(`${config.endpoint}/visitors`, {\n method: 'POST',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n const data = (await response.json()) as Visitor;\n if (data.id) {\n config.storage.setItem(keys.visitor_id, data.id);\n }\n return data;\n}\n\nasync function getOrCreateVisitor(): Promise<Visitor> {\n const visitorId = config.storage.getItem(keys.visitor_id);\n if (visitorId && visitorId !== 'undefined') {\n const response = await fetch(`${config.endpoint}/visitors/${visitorId}`, {\n method: '
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["config","keys","cache"],"sources":["../../src/visitor/index.ts"],"sourcesContent":["import { fetch } from '@shware/utils';\nimport { keys } from '../constants/storage';\nimport type { CreateVisitorDTO, UpdateVisitorDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport type { Visitor, VisitorProperties } from './types';\n\nasync function createVisitor(): Promise<Visitor> {\n const tags = await config.getTags();\n const dto: CreateVisitorDTO = {\n device_id: await config.getDeviceId(),\n platform: config.platform,\n environment: config.environment,\n tags,\n properties: tags as VisitorProperties,\n };\n\n const response = await fetch(`${config.endpoint}/visitors`, {\n method: 'POST',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n const data = (await response.json()) as Visitor;\n if (data.id) {\n config.storage.setItem(keys.visitor_id, data.id);\n }\n return data;\n}\n\nasync function getOrCreateVisitor(): Promise<Visitor> {\n const visitorId = config.storage.getItem(keys.visitor_id);\n if (visitorId && visitorId !== 'undefined') {\n // PATCH, not GET: `tags` is the last-touch counterpart to `initial_tags`,\n // and the only thing that ever refreshed it was `setVisitor`, which hosts\n // call when they identify a user. A visitor who never signs in therefore\n // kept the browser, screen, and release captured on their first ever page\n // load — for the rest of their life — leaving `tags` permanently equal to\n // `initial_tags` and the two columns pointless.\n //\n // Costs nothing extra: this replaces the request that was already here.\n const tags = await config.getTags();\n const response = await fetch(`${config.endpoint}/visitors/${visitorId}`, {\n method: 'PATCH',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify({ tags } satisfies UpdateVisitorDTO),\n });\n\n if (!response.ok) return createVisitor();\n const data = (await response.json()) as Visitor;\n\n if (data.id) {\n config.storage.setItem(keys.visitor_id, data.id);\n }\n return data;\n } else {\n return createVisitor();\n }\n}\n\nlet visitorFetcher: Promise<Visitor> | null = null;\n\nexport async function getVisitor(): Promise<Visitor> {\n if (cache.visitor) return cache.visitor;\n if (visitorFetcher) return visitorFetcher;\n visitorFetcher = getOrCreateVisitor();\n cache.visitor = await visitorFetcher;\n visitorFetcher = null;\n return cache.visitor;\n}\n\nexport async function setVisitor(dto: Omit<UpdateVisitorDTO, 'tags'>) {\n const { id } = await getVisitor();\n const tags = await config.getTags();\n const body: UpdateVisitorDTO = { ...dto, tags };\n const response = await fetch(`${config.endpoint}/visitors/${id}`, {\n method: 'PATCH',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(body),\n });\n\n if (!response.ok) throw new Error('Failed to set visitor');\n const data = (await response.json()) as Visitor;\n\n config.thirdPartyUserSetters.forEach((setter) => setter(body));\n cache.visitor = data;\n return data;\n}\n"],"mappings":";;;;;AAMA,eAAe,gBAAkC;CAC/C,MAAM,OAAO,MAAMA,oBAAAA,OAAO,QAAQ;CAClC,MAAM,MAAwB;EAC5B,WAAW,MAAMA,oBAAAA,OAAO,YAAY;EACpC,UAAUA,oBAAAA,OAAO;EACjB,aAAaA,oBAAAA,OAAO;EACpB;EACA,YAAY;CACd;CASA,MAAM,OAAQ,OAAM,OAAA,GAAA,cAAA,MAAA,CAPS,GAAGA,oBAAAA,OAAO,SAAS,YAAY;EAC1D,QAAQ;EACR,aAAa;EACb,SAAS,MAAMA,oBAAAA,OAAO,WAAW;EACjC,MAAM,KAAK,UAAU,GAAG;CAC1B,CAAC,EAAA,CAE4B,KAAK;CAClC,IAAI,KAAK,IACP,oBAAA,OAAO,QAAQ,QAAQC,0BAAAA,KAAK,YAAY,KAAK,EAAE;CAEjD,OAAO;AACT;AAEA,eAAe,qBAAuC;CACpD,MAAM,YAAYD,oBAAAA,OAAO,QAAQ,QAAQC,0BAAAA,KAAK,UAAU;CACxD,IAAI,aAAa,cAAc,aAAa;EAS1C,MAAM,OAAO,MAAMD,oBAAAA,OAAO,QAAQ;EAClC,MAAM,WAAW,OAAA,GAAA,cAAA,MAAA,CAAY,GAAGA,oBAAAA,OAAO,SAAS,YAAY,aAAa;GACvE,QAAQ;GACR,aAAa;GACb,SAAS,MAAMA,oBAAAA,OAAO,WAAW;GACjC,MAAM,KAAK,UAAU,EAAE,KAAK,CAA4B;EAC1D,CAAC;EAED,IAAI,CAAC,SAAS,IAAI,OAAO,cAAc;EACvC,MAAM,OAAQ,MAAM,SAAS,KAAK;EAElC,IAAI,KAAK,IACP,oBAAA,OAAO,QAAQ,QAAQC,0BAAAA,KAAK,YAAY,KAAK,EAAE;EAEjD,OAAO;CACT,OACE,OAAO,cAAc;AAEzB;AAEA,IAAI,iBAA0C;AAE9C,eAAsB,aAA+B;CACnD,IAAIC,oBAAAA,MAAM,SAAS,OAAOA,oBAAAA,MAAM;CAChC,IAAI,gBAAgB,OAAO;CAC3B,iBAAiB,mBAAmB;CACpC,oBAAA,MAAM,UAAU,MAAM;CACtB,iBAAiB;CACjB,OAAOA,oBAAAA,MAAM;AACf;AAEA,eAAsB,WAAW,KAAqC;CACpE,MAAM,EAAE,OAAO,MAAM,WAAW;CAChC,MAAM,OAAO,MAAMF,oBAAAA,OAAO,QAAQ;CAClC,MAAM,OAAyB;EAAE,GAAG;EAAK;CAAK;CAC9C,MAAM,WAAW,OAAA,GAAA,cAAA,MAAA,CAAY,GAAGA,oBAAAA,OAAO,SAAS,YAAY,MAAM;EAChE,QAAQ;EACR,aAAa;EACb,SAAS,MAAMA,oBAAAA,OAAO,WAAW;EACjC,MAAM,KAAK,UAAU,IAAI;CAC3B,CAAC;CAED,IAAI,CAAC,SAAS,IAAI,MAAM,IAAI,MAAM,uBAAuB;CACzD,MAAM,OAAQ,MAAM,SAAS,KAAK;CAElC,oBAAA,OAAO,sBAAsB,SAAS,WAAW,OAAO,IAAI,CAAC;CAC7D,oBAAA,MAAM,UAAU;CAChB,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/visitor/index.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/visitor/index.ts"],"mappings":";;;iBA+DsB,cAAc,QAAQ;iBAStB,WAAW,KAAK,KAAK,4BAAyB,QAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/visitor/index.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/visitor/index.ts"],"mappings":";;;iBA+DsB,cAAc,QAAQ;iBAStB,WAAW,KAAK,KAAK,4BAAyB,QAAA"}
|
package/dist/visitor/index.mjs
CHANGED
|
@@ -23,10 +23,12 @@ async function createVisitor() {
|
|
|
23
23
|
async function getOrCreateVisitor() {
|
|
24
24
|
const visitorId = config.storage.getItem(keys.visitor_id);
|
|
25
25
|
if (visitorId && visitorId !== "undefined") {
|
|
26
|
+
const tags = await config.getTags();
|
|
26
27
|
const response = await fetch(`${config.endpoint}/visitors/${visitorId}`, {
|
|
27
|
-
method: "
|
|
28
|
+
method: "PATCH",
|
|
28
29
|
credentials: "include",
|
|
29
|
-
headers: await config.getHeaders()
|
|
30
|
+
headers: await config.getHeaders(),
|
|
31
|
+
body: JSON.stringify({ tags })
|
|
30
32
|
});
|
|
31
33
|
if (!response.ok) return createVisitor();
|
|
32
34
|
const data = await response.json();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/visitor/index.ts"],"sourcesContent":["import { fetch } from '@shware/utils';\nimport { keys } from '../constants/storage';\nimport type { CreateVisitorDTO, UpdateVisitorDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport type { Visitor, VisitorProperties } from './types';\n\nasync function createVisitor(): Promise<Visitor> {\n const tags = await config.getTags();\n const dto: CreateVisitorDTO = {\n device_id: await config.getDeviceId(),\n platform: config.platform,\n environment: config.environment,\n tags,\n properties: tags as VisitorProperties,\n };\n\n const response = await fetch(`${config.endpoint}/visitors`, {\n method: 'POST',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n const data = (await response.json()) as Visitor;\n if (data.id) {\n config.storage.setItem(keys.visitor_id, data.id);\n }\n return data;\n}\n\nasync function getOrCreateVisitor(): Promise<Visitor> {\n const visitorId = config.storage.getItem(keys.visitor_id);\n if (visitorId && visitorId !== 'undefined') {\n const response = await fetch(`${config.endpoint}/visitors/${visitorId}`, {\n method: '
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/visitor/index.ts"],"sourcesContent":["import { fetch } from '@shware/utils';\nimport { keys } from '../constants/storage';\nimport type { CreateVisitorDTO, UpdateVisitorDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport type { Visitor, VisitorProperties } from './types';\n\nasync function createVisitor(): Promise<Visitor> {\n const tags = await config.getTags();\n const dto: CreateVisitorDTO = {\n device_id: await config.getDeviceId(),\n platform: config.platform,\n environment: config.environment,\n tags,\n properties: tags as VisitorProperties,\n };\n\n const response = await fetch(`${config.endpoint}/visitors`, {\n method: 'POST',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n const data = (await response.json()) as Visitor;\n if (data.id) {\n config.storage.setItem(keys.visitor_id, data.id);\n }\n return data;\n}\n\nasync function getOrCreateVisitor(): Promise<Visitor> {\n const visitorId = config.storage.getItem(keys.visitor_id);\n if (visitorId && visitorId !== 'undefined') {\n // PATCH, not GET: `tags` is the last-touch counterpart to `initial_tags`,\n // and the only thing that ever refreshed it was `setVisitor`, which hosts\n // call when they identify a user. A visitor who never signs in therefore\n // kept the browser, screen, and release captured on their first ever page\n // load — for the rest of their life — leaving `tags` permanently equal to\n // `initial_tags` and the two columns pointless.\n //\n // Costs nothing extra: this replaces the request that was already here.\n const tags = await config.getTags();\n const response = await fetch(`${config.endpoint}/visitors/${visitorId}`, {\n method: 'PATCH',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify({ tags } satisfies UpdateVisitorDTO),\n });\n\n if (!response.ok) return createVisitor();\n const data = (await response.json()) as Visitor;\n\n if (data.id) {\n config.storage.setItem(keys.visitor_id, data.id);\n }\n return data;\n } else {\n return createVisitor();\n }\n}\n\nlet visitorFetcher: Promise<Visitor> | null = null;\n\nexport async function getVisitor(): Promise<Visitor> {\n if (cache.visitor) return cache.visitor;\n if (visitorFetcher) return visitorFetcher;\n visitorFetcher = getOrCreateVisitor();\n cache.visitor = await visitorFetcher;\n visitorFetcher = null;\n return cache.visitor;\n}\n\nexport async function setVisitor(dto: Omit<UpdateVisitorDTO, 'tags'>) {\n const { id } = await getVisitor();\n const tags = await config.getTags();\n const body: UpdateVisitorDTO = { ...dto, tags };\n const response = await fetch(`${config.endpoint}/visitors/${id}`, {\n method: 'PATCH',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(body),\n });\n\n if (!response.ok) throw new Error('Failed to set visitor');\n const data = (await response.json()) as Visitor;\n\n config.thirdPartyUserSetters.forEach((setter) => setter(body));\n cache.visitor = data;\n return data;\n}\n"],"mappings":";;;;AAMA,eAAe,gBAAkC;CAC/C,MAAM,OAAO,MAAM,OAAO,QAAQ;CAClC,MAAM,MAAwB;EAC5B,WAAW,MAAM,OAAO,YAAY;EACpC,UAAU,OAAO;EACjB,aAAa,OAAO;EACpB;EACA,YAAY;CACd;CASA,MAAM,OAAQ,OAAM,MAPG,MAAM,GAAG,OAAO,SAAS,YAAY;EAC1D,QAAQ;EACR,aAAa;EACb,SAAS,MAAM,OAAO,WAAW;EACjC,MAAM,KAAK,UAAU,GAAG;CAC1B,CAAC,EAAA,CAE4B,KAAK;CAClC,IAAI,KAAK,IACP,OAAO,QAAQ,QAAQ,KAAK,YAAY,KAAK,EAAE;CAEjD,OAAO;AACT;AAEA,eAAe,qBAAuC;CACpD,MAAM,YAAY,OAAO,QAAQ,QAAQ,KAAK,UAAU;CACxD,IAAI,aAAa,cAAc,aAAa;EAS1C,MAAM,OAAO,MAAM,OAAO,QAAQ;EAClC,MAAM,WAAW,MAAM,MAAM,GAAG,OAAO,SAAS,YAAY,aAAa;GACvE,QAAQ;GACR,aAAa;GACb,SAAS,MAAM,OAAO,WAAW;GACjC,MAAM,KAAK,UAAU,EAAE,KAAK,CAA4B;EAC1D,CAAC;EAED,IAAI,CAAC,SAAS,IAAI,OAAO,cAAc;EACvC,MAAM,OAAQ,MAAM,SAAS,KAAK;EAElC,IAAI,KAAK,IACP,OAAO,QAAQ,QAAQ,KAAK,YAAY,KAAK,EAAE;EAEjD,OAAO;CACT,OACE,OAAO,cAAc;AAEzB;AAEA,IAAI,iBAA0C;AAE9C,eAAsB,aAA+B;CACnD,IAAI,MAAM,SAAS,OAAO,MAAM;CAChC,IAAI,gBAAgB,OAAO;CAC3B,iBAAiB,mBAAmB;CACpC,MAAM,UAAU,MAAM;CACtB,iBAAiB;CACjB,OAAO,MAAM;AACf;AAEA,eAAsB,WAAW,KAAqC;CACpE,MAAM,EAAE,OAAO,MAAM,WAAW;CAChC,MAAM,OAAO,MAAM,OAAO,QAAQ;CAClC,MAAM,OAAyB;EAAE,GAAG;EAAK;CAAK;CAC9C,MAAM,WAAW,MAAM,MAAM,GAAG,OAAO,SAAS,YAAY,MAAM;EAChE,QAAQ;EACR,aAAa;EACb,SAAS,MAAM,OAAO,WAAW;EACjC,MAAM,KAAK,UAAU,IAAI;CAC3B,CAAC;CAED,IAAI,CAAC,SAAS,IAAI,MAAM,IAAI,MAAM,uBAAuB;CACzD,MAAM,OAAQ,MAAM,SAAS,KAAK;CAElC,OAAO,sBAAsB,SAAS,WAAW,OAAO,IAAI,CAAC;CAC7D,MAAM,UAAU;CAChB,OAAO;AACT"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shware/analytics",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
"dependencies": {
|
|
129
129
|
"cookie": "^2.0.1",
|
|
130
130
|
"uuid": "^14.0.1",
|
|
131
|
-
"web-vitals": "^6.0
|
|
131
|
+
"web-vitals": "^6.1.0",
|
|
132
132
|
"zod": "^4.4.3",
|
|
133
133
|
"@shware/utils": "^1.5.2"
|
|
134
134
|
},
|