@shware/analytics 7.2.1 → 7.3.2
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/README.md +12 -0
- 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-web-analytics.cjs +5 -5
- package/dist/hooks/use-web-analytics.cjs.map +1 -1
- package/dist/hooks/use-web-analytics.d.cts.map +1 -1
- package/dist/hooks/use-web-analytics.d.mts.map +1 -1
- package/dist/hooks/use-web-analytics.mjs +5 -5
- package/dist/hooks/use-web-analytics.mjs.map +1 -1
- package/dist/setup/session.cjs +68 -22
- package/dist/setup/session.cjs.map +1 -1
- package/dist/setup/session.d.cts +32 -12
- package/dist/setup/session.d.cts.map +1 -1
- package/dist/setup/session.d.mts +32 -12
- package/dist/setup/session.d.mts.map +1 -1
- package/dist/setup/session.mjs +68 -22
- 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/test/setup.cjs +43 -0
- package/dist/test/setup.cjs.map +1 -0
- package/dist/test/setup.d.cts +19 -0
- package/dist/test/setup.d.cts.map +1 -0
- package/dist/test/setup.d.mts +19 -0
- package/dist/test/setup.d.mts.map +1 -0
- package/dist/test/setup.mjs +40 -0
- package/dist/test/setup.mjs.map +1 -0
- package/dist/third-parties/meta-pixel.cjs +1 -1
- package/dist/third-parties/meta-pixel.cjs.map +1 -1
- package/dist/third-parties/meta-pixel.mjs +1 -1
- package/dist/third-parties/meta-pixel.mjs.map +1 -1
- package/dist/track/fbq.cjs +3 -3
- package/dist/track/fbq.cjs.map +1 -1
- package/dist/track/fbq.d.cts.map +1 -1
- package/dist/track/fbq.d.mts.map +1 -1
- package/dist/track/fbq.mjs +3 -3
- package/dist/track/fbq.mjs.map +1 -1
- package/dist/track/index.cjs +19 -20
- 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 +19 -20
- package/dist/track/index.mjs.map +1 -1
- package/package.json +5 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.cjs","names":[],"sources":["../../src/setup/session.ts"],"sourcesContent":["import { v7 as uuidv7 } from 'uuid';\n\nexport const SESSION_TIMEOUT = 30 * 60 * 1000;\n\
|
|
1
|
+
{"version":3,"file":"session.cjs","names":["config","keys"],"sources":["../../src/setup/session.ts"],"sourcesContent":["import { v7 as uuidv7 } from 'uuid';\nimport { keys } from '../constants/storage';\nimport { config } from './index';\n\nexport const SESSION_TIMEOUT = 30 * 60 * 1000;\n\n/**\n * The part of a session that outlives the page: its identity, and the clock the timeout is\n * measured against. GA4 keeps these in its `_ga_<container>` cookie — alongside the engaged flag,\n * which arrives with `session_engaged` later — and rereads them for every event, which is what\n * makes a session survive a reload, span a whole multipage visit, and stay shared between two\n * tabs of the same site. Holding them in memory instead, as this did, starts a new session on\n * every full navigation and gives every tab one of its own.\n */\ninterface StoredSession {\n id: string;\n /** When the last event was sent. The timeout is measured from here, and only from here. */\n lastEventTime: number;\n}\n\n/** The session an event belongs to, and whether that event is the one that started it. */\ninterface SessionForEvent {\n id: string;\n started: boolean;\n}\n\n/**\n * `<version>.<id>.<lastEventTime>`, compact and cookie-safe rather than JSON: `{`, `\"`\n * and `,` all have to be percent-encoded in a cookie, and a host that wants one session across\n * its subdomains will hand `setupAnalytics` a cookie-backed `storage`, where this has to survive\n * unchanged. A uuidv7 contains no dots, so the record splits cleanly.\n *\n * The version guards a change the parser could not otherwise survive. A field appended to the end\n * does not need one — a short record simply leaves it undefined.\n */\nconst VERSION = '1';\n\nfunction readSession(): StoredSession | undefined {\n const raw = config.storage.getItem(keys.session);\n if (!raw) return undefined;\n\n const [version, id, lastEventTime] = raw.split('.');\n if (version !== VERSION || !id) return undefined;\n\n const parsed = { id, lastEventTime: Number(lastEventTime) };\n if (!Number.isFinite(parsed.lastEventTime)) return undefined;\n return parsed;\n}\n\nfunction writeSession({ id, lastEventTime }: StoredSession) {\n config.storage.setItem(keys.session, `${VERSION}.${id}.${lastEventTime}`);\n}\n\nclass Session {\n /**\n * Engagement is deliberately not stored: it is the time this page has accrued and not yet\n * reported, so it belongs to the page, not to the session. GA4 draws the same line — its cookie\n * carries the session, while the engagement timer lives and dies with the document.\n *\n * `lastTickTime` is where the accumulator last settled up, rewritten on every tick — it is not\n * when the session began, and nothing here needs to know that.\n */\n private lastTickTime: number;\n private accumulatedTime: number;\n\n private active: boolean;\n private visible: boolean;\n private focused: boolean;\n\n constructor() {\n this.lastTickTime = Date.now();\n this.accumulatedTime = 0;\n\n this.active = true;\n this.visible = typeof document !== 'undefined' ? document.visibilityState === 'visible' : true;\n this.focused = typeof document !== 'undefined' ? document.hasFocus() : true;\n }\n\n /**\n * The session a batch of events belongs to: read the stored one, start a new one if it has\n * timed out or there is none, stamp it with when those events happened and write it back. Every\n * event goes through here, exactly as GA4 rereads and rewrites its cookie per event — caching\n * the session in memory would put the tabs back out of step with each other.\n */\n touch = (eventTime: number, lastEventTime = eventTime): SessionForEvent => {\n const stored = readSession();\n\n if (stored && eventTime - stored.lastEventTime <= SESSION_TIMEOUT) {\n // `Math.max`, because a batch that waited in a frozen tab can be older than what another\n // tab has since written, and a session must never be shortened by a late arrival.\n writeSession({ ...stored, lastEventTime: Math.max(stored.lastEventTime, lastEventTime) });\n return { id: stored.id, started: false };\n }\n\n // Engagement the previous session accrued but never reported dies with it rather than being\n // handed to its successor. GA4 does the same on `session_start`.\n this.accumulatedTime = 0;\n // Wall clock, not `eventTime`: this anchors the engagement timer for the page in front of the\n // visitor now, which a batch describing something that happened an hour ago says nothing about.\n this.lastTickTime = Date.now();\n\n const session: StoredSession = { id: uuidv7(), lastEventTime };\n writeSession(session);\n return { id: session.id, started: true };\n };\n\n /**\n * The id for an event that must not start a session — the `pagehide` beacon, which reports what\n * the session now ending accrued. A live session is extended, as any event extends it; one\n * already past its timeout still owns that engagement, so its id comes back without being\n * revived into a session no `session_start` ever announced.\n */\n extend = (): string => {\n const stored = readSession();\n if (!stored) return this.touch(Date.now()).id;\n\n const now = Date.now();\n if (now - stored.lastEventTime <= SESSION_TIMEOUT) {\n writeSession({ ...stored, lastEventTime: now });\n }\n return stored.id;\n };\n\n isActive = () => this.active;\n\n updateActive = (active: boolean) => {\n this.active = active;\n };\n\n updateAccumulator = () => {\n const now = Date.now();\n if (this.focused && this.visible && this.active) {\n const delta = now - this.lastTickTime;\n if (delta > 0 && delta < SESSION_TIMEOUT) {\n this.accumulatedTime += delta;\n }\n }\n this.lastTickTime = now;\n };\n\n focus = () => {\n this.updateAccumulator();\n this.focused = true;\n };\n\n blur = () => {\n this.updateAccumulator();\n this.focused = false;\n };\n\n pageshow = () => {\n this.updateAccumulator();\n this.active = true;\n };\n\n pagehide = () => {\n this.updateAccumulator();\n this.active = false;\n };\n\n visibilitychange = (state: DocumentVisibilityState) => {\n this.updateAccumulator();\n this.visible = state === 'visible';\n };\n\n flush = () => {\n this.updateAccumulator();\n const engagementTime = this.accumulatedTime;\n this.accumulatedTime = 0;\n return engagementTime;\n };\n}\n\nlet session: Session | undefined;\n\n/**\n * The session, built the first time something asks for it.\n *\n * Deliberately not a module-scope `new Session()`. The constructor calls\n * `uuidv7()`, `uuid` draws its bytes from `crypto.getRandomValues`, and\n * Cloudflare Workers reject that outside a request handler:\n *\n * Disallowed operation called within global scope. Asynchronous I/O\n * (ex: fetch() or connect()), setting a timeout, and generating random\n * values are not allowed within global scope.\n *\n * Module scope in a Worker is evaluated once when the isolate boots and is\n * then shared by every request that isolate serves, so a random value drawn\n * there would be the same for all of them — which is why the runtime refuses\n * to produce one. A host that server-renders on Workers reaches this module\n * through `track()` on the server as well, and the throw happened as the\n * isolate booted, taking down every route before a component rendered.\n *\n * Everything here is per-visitor browser or app state, so deferring the\n * construction costs nothing and lets the server bundle be evaluated. It does\n * not make any of it safe to use there: this instance, `cache` and `config` are\n * module singletons, so calling `track()` on a server shares one session and one\n * visitor across every request the isolate serves. Import it there; do not track.\n */\nexport function getSession() {\n return (session ??= new Session());\n}\n"],"mappings":";;;;;AAIA,MAAa,kBAAkB,OAAU;;;;;;;;;;AA+BzC,MAAM,UAAU;AAEhB,SAAS,cAAyC;CAChD,MAAM,MAAMA,oBAAAA,OAAO,QAAQ,QAAQC,0BAAAA,KAAK,OAAO;CAC/C,IAAI,CAAC,KAAK,OAAO,KAAA;CAEjB,MAAM,CAAC,SAAS,IAAI,iBAAiB,IAAI,MAAM,GAAG;CAClD,IAAI,YAAY,WAAW,CAAC,IAAI,OAAO,KAAA;CAEvC,MAAM,SAAS;EAAE;EAAI,eAAe,OAAO,aAAa;CAAE;CAC1D,IAAI,CAAC,OAAO,SAAS,OAAO,aAAa,GAAG,OAAO,KAAA;CACnD,OAAO;AACT;AAEA,SAAS,aAAa,EAAE,IAAI,iBAAgC;CAC1D,oBAAA,OAAO,QAAQ,QAAQA,0BAAAA,KAAK,SAAS,GAAG,QAAQ,GAAG,GAAG,GAAG,eAAe;AAC1E;AAEA,IAAM,UAAN,MAAc;CAgBZ,cAAc;EAeL,KAAA,SAAA,WAAmB,gBAAgB,cAA+B;GACzE,MAAM,SAAS,YAAY;GAE3B,IAAI,UAAU,YAAY,OAAO,iBAAA,MAAkC;IAGjE,aAAa;KAAE,GAAG;KAAQ,eAAe,KAAK,IAAI,OAAO,eAAe,aAAa;IAAE,CAAC;IACxF,OAAO;KAAE,IAAI,OAAO;KAAI,SAAS;IAAM;GACzC;GAIA,KAAK,kBAAkB;GAGvB,KAAK,eAAe,KAAK,IAAI;GAE7B,MAAM,UAAyB;IAAE,KAAA,GAAA,KAAA,GAAA,CAAW;IAAG;GAAc;GAC7D,aAAa,OAAO;GACpB,OAAO;IAAE,IAAI,QAAQ;IAAI,SAAS;GAAK;EACzC;EAQuB,KAAA,eAAA;GACrB,MAAM,SAAS,YAAY;GAC3B,IAAI,CAAC,QAAQ,OAAO,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC;GAE3C,MAAM,MAAM,KAAK,IAAI;GACrB,IAAI,MAAM,OAAO,iBAAA,MACf,aAAa;IAAE,GAAG;IAAQ,eAAe;GAAI,CAAC;GAEhD,OAAO,OAAO;EAChB;EAEiB,KAAA,iBAAA,KAAK;EAEN,KAAA,gBAAA,WAAoB;GAClC,KAAK,SAAS;EAChB;EAE0B,KAAA,0BAAA;GACxB,MAAM,MAAM,KAAK,IAAI;GACrB,IAAI,KAAK,WAAW,KAAK,WAAW,KAAK,QAAQ;IAC/C,MAAM,QAAQ,MAAM,KAAK;IACzB,IAAI,QAAQ,KAAK,QAAA,MACf,KAAK,mBAAmB;GAE5B;GACA,KAAK,eAAe;EACtB;EAEc,KAAA,cAAA;GACZ,KAAK,kBAAkB;GACvB,KAAK,UAAU;EACjB;EAEa,KAAA,aAAA;GACX,KAAK,kBAAkB;GACvB,KAAK,UAAU;EACjB;EAEiB,KAAA,iBAAA;GACf,KAAK,kBAAkB;GACvB,KAAK,SAAS;EAChB;EAEiB,KAAA,iBAAA;GACf,KAAK,kBAAkB;GACvB,KAAK,SAAS;EAChB;EAEoB,KAAA,oBAAA,UAAmC;GACrD,KAAK,kBAAkB;GACvB,KAAK,UAAU,UAAU;EAC3B;EAEc,KAAA,cAAA;GACZ,KAAK,kBAAkB;GACvB,MAAM,iBAAiB,KAAK;GAC5B,KAAK,kBAAkB;GACvB,OAAO;EACT;EApGE,KAAK,eAAe,KAAK,IAAI;EAC7B,KAAK,kBAAkB;EAEvB,KAAK,SAAS;EACd,KAAK,UAAU,OAAO,aAAa,cAAc,SAAS,oBAAoB,YAAY;EAC1F,KAAK,UAAU,OAAO,aAAa,cAAc,SAAS,SAAS,IAAI;CACzE;AA+FF;AAEA,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;AA0BJ,SAAgB,aAAa;CAC3B,OAAQ,YAAY,IAAI,QAAQ;AAClC"}
|
package/dist/setup/session.d.cts
CHANGED
|
@@ -1,22 +1,41 @@
|
|
|
1
1
|
//#region src/setup/session.d.ts
|
|
2
2
|
declare const SESSION_TIMEOUT: number;
|
|
3
|
+
/** The session an event belongs to, and whether that event is the one that started it. */
|
|
4
|
+
interface SessionForEvent {
|
|
5
|
+
id: string;
|
|
6
|
+
started: boolean;
|
|
7
|
+
}
|
|
3
8
|
declare class Session {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Engagement is deliberately not stored: it is the time this page has accrued and not yet
|
|
11
|
+
* reported, so it belongs to the page, not to the session. GA4 draws the same line — its cookie
|
|
12
|
+
* carries the session, while the engagement timer lives and dies with the document.
|
|
13
|
+
*
|
|
14
|
+
* `lastTickTime` is where the accumulator last settled up, rewritten on every tick — it is not
|
|
15
|
+
* when the session began, and nothing here needs to know that.
|
|
16
|
+
*/
|
|
17
|
+
private lastTickTime;
|
|
7
18
|
private accumulatedTime;
|
|
8
19
|
private active;
|
|
9
20
|
private visible;
|
|
10
21
|
private focused;
|
|
11
22
|
constructor();
|
|
12
|
-
|
|
23
|
+
/**
|
|
24
|
+
* The session a batch of events belongs to: read the stored one, start a new one if it has
|
|
25
|
+
* timed out or there is none, stamp it with when those events happened and write it back. Every
|
|
26
|
+
* event goes through here, exactly as GA4 rereads and rewrites its cookie per event — caching
|
|
27
|
+
* the session in memory would put the tabs back out of step with each other.
|
|
28
|
+
*/
|
|
29
|
+
touch: (eventTime: number, lastEventTime?: number) => SessionForEvent;
|
|
30
|
+
/**
|
|
31
|
+
* The id for an event that must not start a session — the `pagehide` beacon, which reports what
|
|
32
|
+
* the session now ending accrued. A live session is extended, as any event extends it; one
|
|
33
|
+
* already past its timeout still owns that engagement, so its id comes back without being
|
|
34
|
+
* revived into a session no `session_start` ever announced.
|
|
35
|
+
*/
|
|
36
|
+
extend: () => string;
|
|
13
37
|
isActive: () => boolean;
|
|
14
|
-
isVisible: () => boolean;
|
|
15
|
-
isFocused: () => boolean;
|
|
16
|
-
isExpired: () => boolean;
|
|
17
|
-
updateLastActiveTime: () => void;
|
|
18
38
|
updateActive: (active: boolean) => void;
|
|
19
|
-
refresh: () => void;
|
|
20
39
|
updateAccumulator: () => void;
|
|
21
40
|
focus: () => void;
|
|
22
41
|
blur: () => void;
|
|
@@ -44,9 +63,10 @@ declare class Session {
|
|
|
44
63
|
* isolate booted, taking down every route before a component rendered.
|
|
45
64
|
*
|
|
46
65
|
* Everything here is per-visitor browser or app state, so deferring the
|
|
47
|
-
* construction costs nothing and
|
|
48
|
-
*
|
|
49
|
-
*
|
|
66
|
+
* construction costs nothing and lets the server bundle be evaluated. It does
|
|
67
|
+
* not make any of it safe to use there: this instance, `cache` and `config` are
|
|
68
|
+
* module singletons, so calling `track()` on a server shares one session and one
|
|
69
|
+
* visitor across every request the isolate serves. Import it there; do not track.
|
|
50
70
|
*/
|
|
51
71
|
declare function getSession(): Session;
|
|
52
72
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.cts","names":[],"sources":["../../src/setup/session.ts"],"mappings":";
|
|
1
|
+
{"version":3,"file":"session.d.cts","names":[],"sources":["../../src/setup/session.ts"],"mappings":";cAIa;;UAiBH;EACR;EACA;;cA8BI;;;;;;;;;UASI;UACA;UAEA;UACA;UACA;EAER;;;;;;;EAeA,QAAK,mBAAqB,2BAA8B;;;;;;;EA4BxD;EAWA;EAEA,eAAY;EAIZ;EAWA;EAKA;EAKA;EAKA;EAKA,mBAAgB,OAAW;EAK3B;;;;;;;;;;;;;;;;;;;;;;;;;;iBAkCc,cAAU"}
|
package/dist/setup/session.d.mts
CHANGED
|
@@ -1,22 +1,41 @@
|
|
|
1
1
|
//#region src/setup/session.d.ts
|
|
2
2
|
declare const SESSION_TIMEOUT: number;
|
|
3
|
+
/** The session an event belongs to, and whether that event is the one that started it. */
|
|
4
|
+
interface SessionForEvent {
|
|
5
|
+
id: string;
|
|
6
|
+
started: boolean;
|
|
7
|
+
}
|
|
3
8
|
declare class Session {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Engagement is deliberately not stored: it is the time this page has accrued and not yet
|
|
11
|
+
* reported, so it belongs to the page, not to the session. GA4 draws the same line — its cookie
|
|
12
|
+
* carries the session, while the engagement timer lives and dies with the document.
|
|
13
|
+
*
|
|
14
|
+
* `lastTickTime` is where the accumulator last settled up, rewritten on every tick — it is not
|
|
15
|
+
* when the session began, and nothing here needs to know that.
|
|
16
|
+
*/
|
|
17
|
+
private lastTickTime;
|
|
7
18
|
private accumulatedTime;
|
|
8
19
|
private active;
|
|
9
20
|
private visible;
|
|
10
21
|
private focused;
|
|
11
22
|
constructor();
|
|
12
|
-
|
|
23
|
+
/**
|
|
24
|
+
* The session a batch of events belongs to: read the stored one, start a new one if it has
|
|
25
|
+
* timed out or there is none, stamp it with when those events happened and write it back. Every
|
|
26
|
+
* event goes through here, exactly as GA4 rereads and rewrites its cookie per event — caching
|
|
27
|
+
* the session in memory would put the tabs back out of step with each other.
|
|
28
|
+
*/
|
|
29
|
+
touch: (eventTime: number, lastEventTime?: number) => SessionForEvent;
|
|
30
|
+
/**
|
|
31
|
+
* The id for an event that must not start a session — the `pagehide` beacon, which reports what
|
|
32
|
+
* the session now ending accrued. A live session is extended, as any event extends it; one
|
|
33
|
+
* already past its timeout still owns that engagement, so its id comes back without being
|
|
34
|
+
* revived into a session no `session_start` ever announced.
|
|
35
|
+
*/
|
|
36
|
+
extend: () => string;
|
|
13
37
|
isActive: () => boolean;
|
|
14
|
-
isVisible: () => boolean;
|
|
15
|
-
isFocused: () => boolean;
|
|
16
|
-
isExpired: () => boolean;
|
|
17
|
-
updateLastActiveTime: () => void;
|
|
18
38
|
updateActive: (active: boolean) => void;
|
|
19
|
-
refresh: () => void;
|
|
20
39
|
updateAccumulator: () => void;
|
|
21
40
|
focus: () => void;
|
|
22
41
|
blur: () => void;
|
|
@@ -44,9 +63,10 @@ declare class Session {
|
|
|
44
63
|
* isolate booted, taking down every route before a component rendered.
|
|
45
64
|
*
|
|
46
65
|
* Everything here is per-visitor browser or app state, so deferring the
|
|
47
|
-
* construction costs nothing and
|
|
48
|
-
*
|
|
49
|
-
*
|
|
66
|
+
* construction costs nothing and lets the server bundle be evaluated. It does
|
|
67
|
+
* not make any of it safe to use there: this instance, `cache` and `config` are
|
|
68
|
+
* module singletons, so calling `track()` on a server shares one session and one
|
|
69
|
+
* visitor across every request the isolate serves. Import it there; do not track.
|
|
50
70
|
*/
|
|
51
71
|
declare function getSession(): Session;
|
|
52
72
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.mts","names":[],"sources":["../../src/setup/session.ts"],"mappings":";
|
|
1
|
+
{"version":3,"file":"session.d.mts","names":[],"sources":["../../src/setup/session.ts"],"mappings":";cAIa;;UAiBH;EACR;EACA;;cA8BI;;;;;;;;;UASI;UACA;UAEA;UACA;UACA;EAER;;;;;;;EAeA,QAAK,mBAAqB,2BAA8B;;;;;;;EA4BxD;EAWA;EAEA,eAAY;EAIZ;EAWA;EAKA;EAKA;EAKA;EAKA,mBAAgB,OAAW;EAK3B;;;;;;;;;;;;;;;;;;;;;;;;;;iBAkCc,cAAU"}
|
package/dist/setup/session.mjs
CHANGED
|
@@ -1,34 +1,83 @@
|
|
|
1
|
+
import { config } from "./index.mjs";
|
|
2
|
+
import { keys } from "../constants/storage.mjs";
|
|
1
3
|
import { v7 } from "uuid";
|
|
2
4
|
//#region src/setup/session.ts
|
|
3
5
|
const SESSION_TIMEOUT = 1800 * 1e3;
|
|
6
|
+
/**
|
|
7
|
+
* `<version>.<id>.<lastEventTime>`, compact and cookie-safe rather than JSON: `{`, `"`
|
|
8
|
+
* and `,` all have to be percent-encoded in a cookie, and a host that wants one session across
|
|
9
|
+
* its subdomains will hand `setupAnalytics` a cookie-backed `storage`, where this has to survive
|
|
10
|
+
* unchanged. A uuidv7 contains no dots, so the record splits cleanly.
|
|
11
|
+
*
|
|
12
|
+
* The version guards a change the parser could not otherwise survive. A field appended to the end
|
|
13
|
+
* does not need one — a short record simply leaves it undefined.
|
|
14
|
+
*/
|
|
15
|
+
const VERSION = "1";
|
|
16
|
+
function readSession() {
|
|
17
|
+
const raw = config.storage.getItem(keys.session);
|
|
18
|
+
if (!raw) return void 0;
|
|
19
|
+
const [version, id, lastEventTime] = raw.split(".");
|
|
20
|
+
if (version !== VERSION || !id) return void 0;
|
|
21
|
+
const parsed = {
|
|
22
|
+
id,
|
|
23
|
+
lastEventTime: Number(lastEventTime)
|
|
24
|
+
};
|
|
25
|
+
if (!Number.isFinite(parsed.lastEventTime)) return void 0;
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
function writeSession({ id, lastEventTime }) {
|
|
29
|
+
config.storage.setItem(keys.session, `${VERSION}.${id}.${lastEventTime}`);
|
|
30
|
+
}
|
|
4
31
|
var Session = class {
|
|
5
32
|
constructor() {
|
|
6
|
-
this.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
33
|
+
this.touch = (eventTime, lastEventTime = eventTime) => {
|
|
34
|
+
const stored = readSession();
|
|
35
|
+
if (stored && eventTime - stored.lastEventTime <= 18e5) {
|
|
36
|
+
writeSession({
|
|
37
|
+
...stored,
|
|
38
|
+
lastEventTime: Math.max(stored.lastEventTime, lastEventTime)
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
id: stored.id,
|
|
42
|
+
started: false
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
this.accumulatedTime = 0;
|
|
46
|
+
this.lastTickTime = Date.now();
|
|
47
|
+
const session = {
|
|
48
|
+
id: v7(),
|
|
49
|
+
lastEventTime
|
|
50
|
+
};
|
|
51
|
+
writeSession(session);
|
|
52
|
+
return {
|
|
53
|
+
id: session.id,
|
|
54
|
+
started: true
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
this.extend = () => {
|
|
58
|
+
const stored = readSession();
|
|
59
|
+
if (!stored) return this.touch(Date.now()).id;
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
if (now - stored.lastEventTime <= 18e5) writeSession({
|
|
62
|
+
...stored,
|
|
63
|
+
lastEventTime: now
|
|
64
|
+
});
|
|
65
|
+
return stored.id;
|
|
13
66
|
};
|
|
67
|
+
this.isActive = () => this.active;
|
|
14
68
|
this.updateActive = (active) => {
|
|
15
69
|
this.active = active;
|
|
16
70
|
};
|
|
17
|
-
this.refresh = () => {
|
|
18
|
-
this.id = v7();
|
|
19
|
-
this.lastActiveTime = Date.now();
|
|
20
|
-
};
|
|
21
71
|
this.updateAccumulator = () => {
|
|
22
72
|
const now = Date.now();
|
|
23
73
|
if (this.focused && this.visible && this.active) {
|
|
24
|
-
const delta = now - this.
|
|
74
|
+
const delta = now - this.lastTickTime;
|
|
25
75
|
if (delta > 0 && delta < 18e5) this.accumulatedTime += delta;
|
|
26
76
|
}
|
|
27
|
-
this.
|
|
77
|
+
this.lastTickTime = now;
|
|
28
78
|
};
|
|
29
79
|
this.focus = () => {
|
|
30
80
|
this.updateAccumulator();
|
|
31
|
-
this.updateLastActiveTime();
|
|
32
81
|
this.focused = true;
|
|
33
82
|
};
|
|
34
83
|
this.blur = () => {
|
|
@@ -37,7 +86,6 @@ var Session = class {
|
|
|
37
86
|
};
|
|
38
87
|
this.pageshow = () => {
|
|
39
88
|
this.updateAccumulator();
|
|
40
|
-
this.updateLastActiveTime();
|
|
41
89
|
this.active = true;
|
|
42
90
|
};
|
|
43
91
|
this.pagehide = () => {
|
|
@@ -46,7 +94,6 @@ var Session = class {
|
|
|
46
94
|
};
|
|
47
95
|
this.visibilitychange = (state) => {
|
|
48
96
|
this.updateAccumulator();
|
|
49
|
-
if (state === "visible") this.updateLastActiveTime();
|
|
50
97
|
this.visible = state === "visible";
|
|
51
98
|
};
|
|
52
99
|
this.flush = () => {
|
|
@@ -55,9 +102,7 @@ var Session = class {
|
|
|
55
102
|
this.accumulatedTime = 0;
|
|
56
103
|
return engagementTime;
|
|
57
104
|
};
|
|
58
|
-
this.
|
|
59
|
-
this.startTime = Date.now();
|
|
60
|
-
this.lastActiveTime = Date.now();
|
|
105
|
+
this.lastTickTime = Date.now();
|
|
61
106
|
this.accumulatedTime = 0;
|
|
62
107
|
this.active = true;
|
|
63
108
|
this.visible = typeof document !== "undefined" ? document.visibilityState === "visible" : true;
|
|
@@ -84,9 +129,10 @@ let session;
|
|
|
84
129
|
* isolate booted, taking down every route before a component rendered.
|
|
85
130
|
*
|
|
86
131
|
* Everything here is per-visitor browser or app state, so deferring the
|
|
87
|
-
* construction costs nothing and
|
|
88
|
-
*
|
|
89
|
-
*
|
|
132
|
+
* construction costs nothing and lets the server bundle be evaluated. It does
|
|
133
|
+
* not make any of it safe to use there: this instance, `cache` and `config` are
|
|
134
|
+
* module singletons, so calling `track()` on a server shares one session and one
|
|
135
|
+
* visitor across every request the isolate serves. Import it there; do not track.
|
|
90
136
|
*/
|
|
91
137
|
function getSession() {
|
|
92
138
|
return session ??= new Session();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.mjs","names":["uuidv7"],"sources":["../../src/setup/session.ts"],"sourcesContent":["import { v7 as uuidv7 } from 'uuid';\n\nexport const SESSION_TIMEOUT = 30 * 60 * 1000;\n\
|
|
1
|
+
{"version":3,"file":"session.mjs","names":["uuidv7"],"sources":["../../src/setup/session.ts"],"sourcesContent":["import { v7 as uuidv7 } from 'uuid';\nimport { keys } from '../constants/storage';\nimport { config } from './index';\n\nexport const SESSION_TIMEOUT = 30 * 60 * 1000;\n\n/**\n * The part of a session that outlives the page: its identity, and the clock the timeout is\n * measured against. GA4 keeps these in its `_ga_<container>` cookie — alongside the engaged flag,\n * which arrives with `session_engaged` later — and rereads them for every event, which is what\n * makes a session survive a reload, span a whole multipage visit, and stay shared between two\n * tabs of the same site. Holding them in memory instead, as this did, starts a new session on\n * every full navigation and gives every tab one of its own.\n */\ninterface StoredSession {\n id: string;\n /** When the last event was sent. The timeout is measured from here, and only from here. */\n lastEventTime: number;\n}\n\n/** The session an event belongs to, and whether that event is the one that started it. */\ninterface SessionForEvent {\n id: string;\n started: boolean;\n}\n\n/**\n * `<version>.<id>.<lastEventTime>`, compact and cookie-safe rather than JSON: `{`, `\"`\n * and `,` all have to be percent-encoded in a cookie, and a host that wants one session across\n * its subdomains will hand `setupAnalytics` a cookie-backed `storage`, where this has to survive\n * unchanged. A uuidv7 contains no dots, so the record splits cleanly.\n *\n * The version guards a change the parser could not otherwise survive. A field appended to the end\n * does not need one — a short record simply leaves it undefined.\n */\nconst VERSION = '1';\n\nfunction readSession(): StoredSession | undefined {\n const raw = config.storage.getItem(keys.session);\n if (!raw) return undefined;\n\n const [version, id, lastEventTime] = raw.split('.');\n if (version !== VERSION || !id) return undefined;\n\n const parsed = { id, lastEventTime: Number(lastEventTime) };\n if (!Number.isFinite(parsed.lastEventTime)) return undefined;\n return parsed;\n}\n\nfunction writeSession({ id, lastEventTime }: StoredSession) {\n config.storage.setItem(keys.session, `${VERSION}.${id}.${lastEventTime}`);\n}\n\nclass Session {\n /**\n * Engagement is deliberately not stored: it is the time this page has accrued and not yet\n * reported, so it belongs to the page, not to the session. GA4 draws the same line — its cookie\n * carries the session, while the engagement timer lives and dies with the document.\n *\n * `lastTickTime` is where the accumulator last settled up, rewritten on every tick — it is not\n * when the session began, and nothing here needs to know that.\n */\n private lastTickTime: number;\n private accumulatedTime: number;\n\n private active: boolean;\n private visible: boolean;\n private focused: boolean;\n\n constructor() {\n this.lastTickTime = Date.now();\n this.accumulatedTime = 0;\n\n this.active = true;\n this.visible = typeof document !== 'undefined' ? document.visibilityState === 'visible' : true;\n this.focused = typeof document !== 'undefined' ? document.hasFocus() : true;\n }\n\n /**\n * The session a batch of events belongs to: read the stored one, start a new one if it has\n * timed out or there is none, stamp it with when those events happened and write it back. Every\n * event goes through here, exactly as GA4 rereads and rewrites its cookie per event — caching\n * the session in memory would put the tabs back out of step with each other.\n */\n touch = (eventTime: number, lastEventTime = eventTime): SessionForEvent => {\n const stored = readSession();\n\n if (stored && eventTime - stored.lastEventTime <= SESSION_TIMEOUT) {\n // `Math.max`, because a batch that waited in a frozen tab can be older than what another\n // tab has since written, and a session must never be shortened by a late arrival.\n writeSession({ ...stored, lastEventTime: Math.max(stored.lastEventTime, lastEventTime) });\n return { id: stored.id, started: false };\n }\n\n // Engagement the previous session accrued but never reported dies with it rather than being\n // handed to its successor. GA4 does the same on `session_start`.\n this.accumulatedTime = 0;\n // Wall clock, not `eventTime`: this anchors the engagement timer for the page in front of the\n // visitor now, which a batch describing something that happened an hour ago says nothing about.\n this.lastTickTime = Date.now();\n\n const session: StoredSession = { id: uuidv7(), lastEventTime };\n writeSession(session);\n return { id: session.id, started: true };\n };\n\n /**\n * The id for an event that must not start a session — the `pagehide` beacon, which reports what\n * the session now ending accrued. A live session is extended, as any event extends it; one\n * already past its timeout still owns that engagement, so its id comes back without being\n * revived into a session no `session_start` ever announced.\n */\n extend = (): string => {\n const stored = readSession();\n if (!stored) return this.touch(Date.now()).id;\n\n const now = Date.now();\n if (now - stored.lastEventTime <= SESSION_TIMEOUT) {\n writeSession({ ...stored, lastEventTime: now });\n }\n return stored.id;\n };\n\n isActive = () => this.active;\n\n updateActive = (active: boolean) => {\n this.active = active;\n };\n\n updateAccumulator = () => {\n const now = Date.now();\n if (this.focused && this.visible && this.active) {\n const delta = now - this.lastTickTime;\n if (delta > 0 && delta < SESSION_TIMEOUT) {\n this.accumulatedTime += delta;\n }\n }\n this.lastTickTime = now;\n };\n\n focus = () => {\n this.updateAccumulator();\n this.focused = true;\n };\n\n blur = () => {\n this.updateAccumulator();\n this.focused = false;\n };\n\n pageshow = () => {\n this.updateAccumulator();\n this.active = true;\n };\n\n pagehide = () => {\n this.updateAccumulator();\n this.active = false;\n };\n\n visibilitychange = (state: DocumentVisibilityState) => {\n this.updateAccumulator();\n this.visible = state === 'visible';\n };\n\n flush = () => {\n this.updateAccumulator();\n const engagementTime = this.accumulatedTime;\n this.accumulatedTime = 0;\n return engagementTime;\n };\n}\n\nlet session: Session | undefined;\n\n/**\n * The session, built the first time something asks for it.\n *\n * Deliberately not a module-scope `new Session()`. The constructor calls\n * `uuidv7()`, `uuid` draws its bytes from `crypto.getRandomValues`, and\n * Cloudflare Workers reject that outside a request handler:\n *\n * Disallowed operation called within global scope. Asynchronous I/O\n * (ex: fetch() or connect()), setting a timeout, and generating random\n * values are not allowed within global scope.\n *\n * Module scope in a Worker is evaluated once when the isolate boots and is\n * then shared by every request that isolate serves, so a random value drawn\n * there would be the same for all of them — which is why the runtime refuses\n * to produce one. A host that server-renders on Workers reaches this module\n * through `track()` on the server as well, and the throw happened as the\n * isolate booted, taking down every route before a component rendered.\n *\n * Everything here is per-visitor browser or app state, so deferring the\n * construction costs nothing and lets the server bundle be evaluated. It does\n * not make any of it safe to use there: this instance, `cache` and `config` are\n * module singletons, so calling `track()` on a server shares one session and one\n * visitor across every request the isolate serves. Import it there; do not track.\n */\nexport function getSession() {\n return (session ??= new Session());\n}\n"],"mappings":";;;;AAIA,MAAa,kBAAkB,OAAU;;;;;;;;;;AA+BzC,MAAM,UAAU;AAEhB,SAAS,cAAyC;CAChD,MAAM,MAAM,OAAO,QAAQ,QAAQ,KAAK,OAAO;CAC/C,IAAI,CAAC,KAAK,OAAO,KAAA;CAEjB,MAAM,CAAC,SAAS,IAAI,iBAAiB,IAAI,MAAM,GAAG;CAClD,IAAI,YAAY,WAAW,CAAC,IAAI,OAAO,KAAA;CAEvC,MAAM,SAAS;EAAE;EAAI,eAAe,OAAO,aAAa;CAAE;CAC1D,IAAI,CAAC,OAAO,SAAS,OAAO,aAAa,GAAG,OAAO,KAAA;CACnD,OAAO;AACT;AAEA,SAAS,aAAa,EAAE,IAAI,iBAAgC;CAC1D,OAAO,QAAQ,QAAQ,KAAK,SAAS,GAAG,QAAQ,GAAG,GAAG,GAAG,eAAe;AAC1E;AAEA,IAAM,UAAN,MAAc;CAgBZ,cAAc;EAeL,KAAA,SAAA,WAAmB,gBAAgB,cAA+B;GACzE,MAAM,SAAS,YAAY;GAE3B,IAAI,UAAU,YAAY,OAAO,iBAAA,MAAkC;IAGjE,aAAa;KAAE,GAAG;KAAQ,eAAe,KAAK,IAAI,OAAO,eAAe,aAAa;IAAE,CAAC;IACxF,OAAO;KAAE,IAAI,OAAO;KAAI,SAAS;IAAM;GACzC;GAIA,KAAK,kBAAkB;GAGvB,KAAK,eAAe,KAAK,IAAI;GAE7B,MAAM,UAAyB;IAAE,IAAIA,GAAO;IAAG;GAAc;GAC7D,aAAa,OAAO;GACpB,OAAO;IAAE,IAAI,QAAQ;IAAI,SAAS;GAAK;EACzC;EAQuB,KAAA,eAAA;GACrB,MAAM,SAAS,YAAY;GAC3B,IAAI,CAAC,QAAQ,OAAO,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC;GAE3C,MAAM,MAAM,KAAK,IAAI;GACrB,IAAI,MAAM,OAAO,iBAAA,MACf,aAAa;IAAE,GAAG;IAAQ,eAAe;GAAI,CAAC;GAEhD,OAAO,OAAO;EAChB;EAEiB,KAAA,iBAAA,KAAK;EAEN,KAAA,gBAAA,WAAoB;GAClC,KAAK,SAAS;EAChB;EAE0B,KAAA,0BAAA;GACxB,MAAM,MAAM,KAAK,IAAI;GACrB,IAAI,KAAK,WAAW,KAAK,WAAW,KAAK,QAAQ;IAC/C,MAAM,QAAQ,MAAM,KAAK;IACzB,IAAI,QAAQ,KAAK,QAAA,MACf,KAAK,mBAAmB;GAE5B;GACA,KAAK,eAAe;EACtB;EAEc,KAAA,cAAA;GACZ,KAAK,kBAAkB;GACvB,KAAK,UAAU;EACjB;EAEa,KAAA,aAAA;GACX,KAAK,kBAAkB;GACvB,KAAK,UAAU;EACjB;EAEiB,KAAA,iBAAA;GACf,KAAK,kBAAkB;GACvB,KAAK,SAAS;EAChB;EAEiB,KAAA,iBAAA;GACf,KAAK,kBAAkB;GACvB,KAAK,SAAS;EAChB;EAEoB,KAAA,oBAAA,UAAmC;GACrD,KAAK,kBAAkB;GACvB,KAAK,UAAU,UAAU;EAC3B;EAEc,KAAA,cAAA;GACZ,KAAK,kBAAkB;GACvB,MAAM,iBAAiB,KAAK;GAC5B,KAAK,kBAAkB;GACvB,OAAO;EACT;EApGE,KAAK,eAAe,KAAK,IAAI;EAC7B,KAAK,kBAAkB;EAEvB,KAAK,SAAS;EACd,KAAK,UAAU,OAAO,aAAa,cAAc,SAAS,oBAAoB,YAAY;EAC1F,KAAK,UAAU,OAAO,aAAa,cAAc,SAAS,SAAS,IAAI;CACzE;AA+FF;AAEA,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;AA0BJ,SAAgB,aAAa;CAC3B,OAAQ,YAAY,IAAI,QAAQ;AAClC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.cjs","names":["resolveClickIdCookies","toSetCookieHeaders"],"sources":["../../src/tanstack/middleware.ts"],"sourcesContent":["import { createMiddleware } from '@tanstack/react-start';\nimport { resolveClickIdCookies, toSetCookieHeaders } from '../click-id/index';\n\nexport interface ClickIdMiddlewareOptions {\n /** `Domain` attribute for the cookies, e.g. `.
|
|
1
|
+
{"version":3,"file":"middleware.cjs","names":["resolveClickIdCookies","toSetCookieHeaders"],"sources":["../../src/tanstack/middleware.ts"],"sourcesContent":["import { createMiddleware } from '@tanstack/react-start';\nimport { resolveClickIdCookies, toSetCookieHeaders } from '../click-id/index';\n\nexport interface ClickIdMiddlewareOptions {\n /** `Domain` attribute for the 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` (com=0, example.com=1, www.example.com=2). Default 1. */\n subdomainIndex?: number;\n /**\n * Re-issue a still-valid `_fbc` on every request as an ITP self-heal (restores the long-lived\n * HTTP cookie if the Meta Pixel's `document.cookie` write re-capped it to 24h in Safari). On by\n * default. Note it attaches a per-user `Set-Cookie` — and thus `no-store` — to every page\n * response carrying an `_fbc`, defeating CDN caching of those pages; set false to strictly follow\n * Meta's conditional-write rule and keep them cacheable. See {@link resolveClickIdCookies}.\n */\n refresh?: boolean;\n /**\n * Override the `Cache-Control` of a response we attach cookies to (default `private, no-store`).\n * A per-user `Set-Cookie` must never end up on a shared-cache entry, or one visitor's `_fbc` would\n * be served to everyone. Only set this false if you guarantee these responses are never cached.\n */\n cacheControl?: string | false;\n /**\n * Consent gate. Return false to skip setting cookies for this request (e.g. before the visitor has\n * granted consent where required). Runs per request with the incoming `Request`.\n */\n shouldPersist?: (request: Request) => boolean;\n}\n\n/**\n * TanStack Start request middleware that persists ad click-id cookies (`_fbc`, `_rdt_cid`) on the\n * document response.\n *\n * Setting `_fbc` here — on the top document via an HTTP `Set-Cookie` header, before any client JS\n * runs — is what Meta officially recommends and the only reliable way to keep the cookie alive for\n * 90 days in Safari: ITP caps JavaScript-set cookies on a fbclid-decorated landing page to 24\n * hours, and a document response is never classified as CNAME/IP cloaking (it is the reference the\n * browser measures cloaking against).\n *\n * Register it as a global request middleware:\n * ```ts\n * // start.ts\n * import { createStart } from '@tanstack/react-start'\n * import { clickIdMiddleware } from '@shware/analytics/tanstack'\n * export const startInstance = createStart(() => ({ requestMiddleware: [clickIdMiddleware] }))\n * ```\n *\n * reference: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc\n */\nexport function createClickIdMiddleware(options: ClickIdMiddlewareOptions = {}) {\n const { cacheControl = 'private, no-store' } = options;\n\n return createMiddleware({ type: 'request' }).server(async ({ request, next, handlerType }) => {\n const result = await next();\n\n // Skip serverFn RPC responses. 'router' covers SSR document requests *and* custom server\n // routes (API endpoints) — those also get cookies when the URL carries a click id.\n if (handlerType !== 'router') return result;\n if (options.shouldPersist && !options.shouldPersist(request)) return result;\n\n const { cookies } = resolveClickIdCookies({\n url: request.url,\n cookieHeader: request.headers.get('cookie'),\n domain: options.domain,\n secure: options.secure,\n subdomainIndex: options.subdomainIndex,\n refresh: options.refresh ?? true,\n });\n\n if (cookies.length > 0) {\n for (const header of toSetCookieHeaders(cookies)) {\n result.response.headers.append('set-cookie', header);\n }\n if (cacheControl !== false) {\n result.response.headers.set('cache-control', cacheControl);\n }\n }\n\n return result;\n });\n}\n\n/** Ready-to-register middleware with default options. */\nexport const clickIdMiddleware = createClickIdMiddleware();\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAmDA,SAAgB,wBAAwB,UAAoC,CAAC,GAAG;CAC9E,MAAM,EAAE,eAAe,wBAAwB;CAE/C,QAAA,GAAA,sBAAA,iBAAA,CAAwB,EAAE,MAAM,UAAU,CAAC,CAAC,CAAC,OAAO,OAAO,EAAE,SAAS,MAAM,kBAAkB;EAC5F,MAAM,SAAS,MAAM,KAAK;EAI1B,IAAI,gBAAgB,UAAU,OAAO;EACrC,IAAI,QAAQ,iBAAiB,CAAC,QAAQ,cAAc,OAAO,GAAG,OAAO;EAErE,MAAM,EAAE,YAAYA,uBAAAA,sBAAsB;GACxC,KAAK,QAAQ;GACb,cAAc,QAAQ,QAAQ,IAAI,QAAQ;GAC1C,QAAQ,QAAQ;GAChB,QAAQ,QAAQ;GAChB,gBAAgB,QAAQ;GACxB,SAAS,QAAQ,WAAW;EAC9B,CAAC;EAED,IAAI,QAAQ,SAAS,GAAG;GACtB,KAAK,MAAM,UAAUC,uBAAAA,mBAAmB,OAAO,GAC7C,OAAO,SAAS,QAAQ,OAAO,cAAc,MAAM;GAErD,IAAI,iBAAiB,OACnB,OAAO,SAAS,QAAQ,IAAI,iBAAiB,YAAY;EAE7D;EAEA,OAAO;CACT,CAAC;AACH;;AAGA,MAAa,oBAAoB,wBAAwB"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
//#region src/tanstack/middleware.d.ts
|
|
2
2
|
interface ClickIdMiddlewareOptions {
|
|
3
|
-
/** `Domain` attribute for the cookies, e.g. `.
|
|
3
|
+
/** `Domain` attribute for the cookies, e.g. `.shware.io`. Omit for a host-only cookie. */
|
|
4
4
|
domain?: string;
|
|
5
5
|
/** `Secure` attribute, default true. Set false only for local http testing. */
|
|
6
6
|
secure?: boolean;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
//#region src/tanstack/middleware.d.ts
|
|
2
2
|
interface ClickIdMiddlewareOptions {
|
|
3
|
-
/** `Domain` attribute for the cookies, e.g. `.
|
|
3
|
+
/** `Domain` attribute for the cookies, e.g. `.shware.io`. Omit for a host-only cookie. */
|
|
4
4
|
domain?: string;
|
|
5
5
|
/** `Secure` attribute, default true. Set false only for local http testing. */
|
|
6
6
|
secure?: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.mjs","names":[],"sources":["../../src/tanstack/middleware.ts"],"sourcesContent":["import { createMiddleware } from '@tanstack/react-start';\nimport { resolveClickIdCookies, toSetCookieHeaders } from '../click-id/index';\n\nexport interface ClickIdMiddlewareOptions {\n /** `Domain` attribute for the cookies, e.g. `.
|
|
1
|
+
{"version":3,"file":"middleware.mjs","names":[],"sources":["../../src/tanstack/middleware.ts"],"sourcesContent":["import { createMiddleware } from '@tanstack/react-start';\nimport { resolveClickIdCookies, toSetCookieHeaders } from '../click-id/index';\n\nexport interface ClickIdMiddlewareOptions {\n /** `Domain` attribute for the 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` (com=0, example.com=1, www.example.com=2). Default 1. */\n subdomainIndex?: number;\n /**\n * Re-issue a still-valid `_fbc` on every request as an ITP self-heal (restores the long-lived\n * HTTP cookie if the Meta Pixel's `document.cookie` write re-capped it to 24h in Safari). On by\n * default. Note it attaches a per-user `Set-Cookie` — and thus `no-store` — to every page\n * response carrying an `_fbc`, defeating CDN caching of those pages; set false to strictly follow\n * Meta's conditional-write rule and keep them cacheable. See {@link resolveClickIdCookies}.\n */\n refresh?: boolean;\n /**\n * Override the `Cache-Control` of a response we attach cookies to (default `private, no-store`).\n * A per-user `Set-Cookie` must never end up on a shared-cache entry, or one visitor's `_fbc` would\n * be served to everyone. Only set this false if you guarantee these responses are never cached.\n */\n cacheControl?: string | false;\n /**\n * Consent gate. Return false to skip setting cookies for this request (e.g. before the visitor has\n * granted consent where required). Runs per request with the incoming `Request`.\n */\n shouldPersist?: (request: Request) => boolean;\n}\n\n/**\n * TanStack Start request middleware that persists ad click-id cookies (`_fbc`, `_rdt_cid`) on the\n * document response.\n *\n * Setting `_fbc` here — on the top document via an HTTP `Set-Cookie` header, before any client JS\n * runs — is what Meta officially recommends and the only reliable way to keep the cookie alive for\n * 90 days in Safari: ITP caps JavaScript-set cookies on a fbclid-decorated landing page to 24\n * hours, and a document response is never classified as CNAME/IP cloaking (it is the reference the\n * browser measures cloaking against).\n *\n * Register it as a global request middleware:\n * ```ts\n * // start.ts\n * import { createStart } from '@tanstack/react-start'\n * import { clickIdMiddleware } from '@shware/analytics/tanstack'\n * export const startInstance = createStart(() => ({ requestMiddleware: [clickIdMiddleware] }))\n * ```\n *\n * reference: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc\n */\nexport function createClickIdMiddleware(options: ClickIdMiddlewareOptions = {}) {\n const { cacheControl = 'private, no-store' } = options;\n\n return createMiddleware({ type: 'request' }).server(async ({ request, next, handlerType }) => {\n const result = await next();\n\n // Skip serverFn RPC responses. 'router' covers SSR document requests *and* custom server\n // routes (API endpoints) — those also get cookies when the URL carries a click id.\n if (handlerType !== 'router') return result;\n if (options.shouldPersist && !options.shouldPersist(request)) return result;\n\n const { cookies } = resolveClickIdCookies({\n url: request.url,\n cookieHeader: request.headers.get('cookie'),\n domain: options.domain,\n secure: options.secure,\n subdomainIndex: options.subdomainIndex,\n refresh: options.refresh ?? true,\n });\n\n if (cookies.length > 0) {\n for (const header of toSetCookieHeaders(cookies)) {\n result.response.headers.append('set-cookie', header);\n }\n if (cacheControl !== false) {\n result.response.headers.set('cache-control', cacheControl);\n }\n }\n\n return result;\n });\n}\n\n/** Ready-to-register middleware with default options. */\nexport const clickIdMiddleware = createClickIdMiddleware();\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAmDA,SAAgB,wBAAwB,UAAoC,CAAC,GAAG;CAC9E,MAAM,EAAE,eAAe,wBAAwB;CAE/C,OAAO,iBAAiB,EAAE,MAAM,UAAU,CAAC,CAAC,CAAC,OAAO,OAAO,EAAE,SAAS,MAAM,kBAAkB;EAC5F,MAAM,SAAS,MAAM,KAAK;EAI1B,IAAI,gBAAgB,UAAU,OAAO;EACrC,IAAI,QAAQ,iBAAiB,CAAC,QAAQ,cAAc,OAAO,GAAG,OAAO;EAErE,MAAM,EAAE,YAAY,sBAAsB;GACxC,KAAK,QAAQ;GACb,cAAc,QAAQ,QAAQ,IAAI,QAAQ;GAC1C,QAAQ,QAAQ;GAChB,QAAQ,QAAQ;GAChB,gBAAgB,QAAQ;GACxB,SAAS,QAAQ,WAAW;EAC9B,CAAC;EAED,IAAI,QAAQ,SAAS,GAAG;GACtB,KAAK,MAAM,UAAU,mBAAmB,OAAO,GAC7C,OAAO,SAAS,QAAQ,OAAO,cAAc,MAAM;GAErD,IAAI,iBAAiB,OACnB,OAAO,SAAS,QAAQ,IAAI,iBAAiB,YAAY;EAE7D;EAEA,OAAO;CACT,CAAC;AACH;;AAGA,MAAa,oBAAoB,wBAAwB"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/test/setup.ts
|
|
3
|
+
/**
|
|
4
|
+
* The SDK's state — `config`, `cache`, the session singleton, the track queue — lives in module
|
|
5
|
+
* scope, exactly as it does in a browser. Tests therefore load the modules fresh per test with
|
|
6
|
+
* `vi.resetModules()` and dynamic imports; this file only provides the pieces every test hands to
|
|
7
|
+
* `setupAnalytics`, and is itself imported dynamically so it joins the same module graph.
|
|
8
|
+
*/
|
|
9
|
+
function memoryStorage(seed = {}) {
|
|
10
|
+
const map = new Map(Object.entries(seed));
|
|
11
|
+
return {
|
|
12
|
+
map,
|
|
13
|
+
getItem: (key) => map.get(key) ?? null,
|
|
14
|
+
setItem: (key, value) => {
|
|
15
|
+
map.set(key, value);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function baseOptions(overrides = {}) {
|
|
20
|
+
return {
|
|
21
|
+
release: "1.0.0",
|
|
22
|
+
storage: memoryStorage(),
|
|
23
|
+
endpoint: "https://api.test",
|
|
24
|
+
platform: "web",
|
|
25
|
+
environment: "production",
|
|
26
|
+
getTags: () => ({}),
|
|
27
|
+
getDeviceId: () => "device-1",
|
|
28
|
+
...overrides
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** A minimal ok Response whose json body is `data`. */
|
|
32
|
+
function jsonResponse(data, status = 200) {
|
|
33
|
+
return new Response(JSON.stringify(data), {
|
|
34
|
+
status,
|
|
35
|
+
headers: { "Content-Type": "application/json" }
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
exports.baseOptions = baseOptions;
|
|
40
|
+
exports.jsonResponse = jsonResponse;
|
|
41
|
+
exports.memoryStorage = memoryStorage;
|
|
42
|
+
|
|
43
|
+
//# sourceMappingURL=setup.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.cjs","names":[],"sources":["../../src/test/setup.ts"],"sourcesContent":["import type { Options } from '../setup/index';\nimport type { TrackTags } from '../track/types';\n\n/**\n * The SDK's state — `config`, `cache`, the session singleton, the track queue — lives in module\n * scope, exactly as it does in a browser. Tests therefore load the modules fresh per test with\n * `vi.resetModules()` and dynamic imports; this file only provides the pieces every test hands to\n * `setupAnalytics`, and is itself imported dynamically so it joins the same module graph.\n */\nexport function memoryStorage(seed: Record<string, string> = {}) {\n const map = new Map(Object.entries(seed));\n return {\n map,\n getItem: (key: string) => map.get(key) ?? null,\n setItem: (key: string, value: string) => {\n map.set(key, value);\n },\n };\n}\n\nexport function baseOptions(overrides: Partial<Options> = {}): Options {\n return {\n release: '1.0.0',\n storage: memoryStorage(),\n endpoint: 'https://api.test',\n platform: 'web',\n environment: 'production',\n getTags: (): TrackTags => ({}),\n getDeviceId: () => 'device-1',\n ...overrides,\n };\n}\n\n/** A minimal ok Response whose json body is `data`. */\nexport function jsonResponse(data: unknown, status = 200) {\n return new Response(JSON.stringify(data), {\n status,\n headers: { 'Content-Type': 'application/json' },\n });\n}\n"],"mappings":";;;;;;;;AASA,SAAgB,cAAc,OAA+B,CAAC,GAAG;CAC/D,MAAM,MAAM,IAAI,IAAI,OAAO,QAAQ,IAAI,CAAC;CACxC,OAAO;EACL;EACA,UAAU,QAAgB,IAAI,IAAI,GAAG,KAAK;EAC1C,UAAU,KAAa,UAAkB;GACvC,IAAI,IAAI,KAAK,KAAK;EACpB;CACF;AACF;AAEA,SAAgB,YAAY,YAA8B,CAAC,GAAY;CACrE,OAAO;EACL,SAAS;EACT,SAAS,cAAc;EACvB,UAAU;EACV,UAAU;EACV,aAAa;EACb,gBAA2B,CAAC;EAC5B,mBAAmB;EACnB,GAAG;CACL;AACF;;AAGA,SAAgB,aAAa,MAAe,SAAS,KAAK;CACxD,OAAO,IAAI,SAAS,KAAK,UAAU,IAAI,GAAG;EACxC;EACA,SAAS,EAAE,gBAAgB,mBAAmB;CAChD,CAAC;AACH"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Options } from "../setup/index.cjs";
|
|
2
|
+
//#region src/test/setup.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The SDK's state — `config`, `cache`, the session singleton, the track queue — lives in module
|
|
5
|
+
* scope, exactly as it does in a browser. Tests therefore load the modules fresh per test with
|
|
6
|
+
* `vi.resetModules()` and dynamic imports; this file only provides the pieces every test hands to
|
|
7
|
+
* `setupAnalytics`, and is itself imported dynamically so it joins the same module graph.
|
|
8
|
+
*/
|
|
9
|
+
declare function memoryStorage(seed?: Record<string, string>): {
|
|
10
|
+
map: Map<string, string>;
|
|
11
|
+
getItem: (key: string) => string | null;
|
|
12
|
+
setItem: (key: string, value: string) => void;
|
|
13
|
+
};
|
|
14
|
+
declare function baseOptions(overrides?: Partial<Options>): Options;
|
|
15
|
+
/** A minimal ok Response whose json body is `data`. */
|
|
16
|
+
declare function jsonResponse(data: unknown, status?: number): Response;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { baseOptions, jsonResponse, memoryStorage };
|
|
19
|
+
//# sourceMappingURL=setup.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.d.cts","names":[],"sources":["../../src/test/setup.ts"],"mappings":";;;;;;;;iBASgB,cAAc,OAAM;OAAA;EAIjB,UAAA;EACA,UAAA,aAAM;;iBAMT,YAAY,YAAW,QAAQ,WAAgB;;iBAc/C,aAAa,eAAe,kBAAY"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Options } from "../setup/index.mjs";
|
|
2
|
+
//#region src/test/setup.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The SDK's state — `config`, `cache`, the session singleton, the track queue — lives in module
|
|
5
|
+
* scope, exactly as it does in a browser. Tests therefore load the modules fresh per test with
|
|
6
|
+
* `vi.resetModules()` and dynamic imports; this file only provides the pieces every test hands to
|
|
7
|
+
* `setupAnalytics`, and is itself imported dynamically so it joins the same module graph.
|
|
8
|
+
*/
|
|
9
|
+
declare function memoryStorage(seed?: Record<string, string>): {
|
|
10
|
+
map: Map<string, string>;
|
|
11
|
+
getItem: (key: string) => string | null;
|
|
12
|
+
setItem: (key: string, value: string) => void;
|
|
13
|
+
};
|
|
14
|
+
declare function baseOptions(overrides?: Partial<Options>): Options;
|
|
15
|
+
/** A minimal ok Response whose json body is `data`. */
|
|
16
|
+
declare function jsonResponse(data: unknown, status?: number): Response;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { baseOptions, jsonResponse, memoryStorage };
|
|
19
|
+
//# sourceMappingURL=setup.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.d.mts","names":[],"sources":["../../src/test/setup.ts"],"mappings":";;;;;;;;iBASgB,cAAc,OAAM;OAAA;EAIjB,UAAA;EACA,UAAA,aAAM;;iBAMT,YAAY,YAAW,QAAQ,WAAgB;;iBAc/C,aAAa,eAAe,kBAAY"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region src/test/setup.ts
|
|
2
|
+
/**
|
|
3
|
+
* The SDK's state — `config`, `cache`, the session singleton, the track queue — lives in module
|
|
4
|
+
* scope, exactly as it does in a browser. Tests therefore load the modules fresh per test with
|
|
5
|
+
* `vi.resetModules()` and dynamic imports; this file only provides the pieces every test hands to
|
|
6
|
+
* `setupAnalytics`, and is itself imported dynamically so it joins the same module graph.
|
|
7
|
+
*/
|
|
8
|
+
function memoryStorage(seed = {}) {
|
|
9
|
+
const map = new Map(Object.entries(seed));
|
|
10
|
+
return {
|
|
11
|
+
map,
|
|
12
|
+
getItem: (key) => map.get(key) ?? null,
|
|
13
|
+
setItem: (key, value) => {
|
|
14
|
+
map.set(key, value);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function baseOptions(overrides = {}) {
|
|
19
|
+
return {
|
|
20
|
+
release: "1.0.0",
|
|
21
|
+
storage: memoryStorage(),
|
|
22
|
+
endpoint: "https://api.test",
|
|
23
|
+
platform: "web",
|
|
24
|
+
environment: "production",
|
|
25
|
+
getTags: () => ({}),
|
|
26
|
+
getDeviceId: () => "device-1",
|
|
27
|
+
...overrides
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** A minimal ok Response whose json body is `data`. */
|
|
31
|
+
function jsonResponse(data, status = 200) {
|
|
32
|
+
return new Response(JSON.stringify(data), {
|
|
33
|
+
status,
|
|
34
|
+
headers: { "Content-Type": "application/json" }
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { baseOptions, jsonResponse, memoryStorage };
|
|
39
|
+
|
|
40
|
+
//# sourceMappingURL=setup.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.mjs","names":[],"sources":["../../src/test/setup.ts"],"sourcesContent":["import type { Options } from '../setup/index';\nimport type { TrackTags } from '../track/types';\n\n/**\n * The SDK's state — `config`, `cache`, the session singleton, the track queue — lives in module\n * scope, exactly as it does in a browser. Tests therefore load the modules fresh per test with\n * `vi.resetModules()` and dynamic imports; this file only provides the pieces every test hands to\n * `setupAnalytics`, and is itself imported dynamically so it joins the same module graph.\n */\nexport function memoryStorage(seed: Record<string, string> = {}) {\n const map = new Map(Object.entries(seed));\n return {\n map,\n getItem: (key: string) => map.get(key) ?? null,\n setItem: (key: string, value: string) => {\n map.set(key, value);\n },\n };\n}\n\nexport function baseOptions(overrides: Partial<Options> = {}): Options {\n return {\n release: '1.0.0',\n storage: memoryStorage(),\n endpoint: 'https://api.test',\n platform: 'web',\n environment: 'production',\n getTags: (): TrackTags => ({}),\n getDeviceId: () => 'device-1',\n ...overrides,\n };\n}\n\n/** A minimal ok Response whose json body is `data`. */\nexport function jsonResponse(data: unknown, status = 200) {\n return new Response(JSON.stringify(data), {\n status,\n headers: { 'Content-Type': 'application/json' },\n });\n}\n"],"mappings":";;;;;;;AASA,SAAgB,cAAc,OAA+B,CAAC,GAAG;CAC/D,MAAM,MAAM,IAAI,IAAI,OAAO,QAAQ,IAAI,CAAC;CACxC,OAAO;EACL;EACA,UAAU,QAAgB,IAAI,IAAI,GAAG,KAAK;EAC1C,UAAU,KAAa,UAAkB;GACvC,IAAI,IAAI,KAAK,KAAK;EACpB;CACF;AACF;AAEA,SAAgB,YAAY,YAA8B,CAAC,GAAY;CACrE,OAAO;EACL,SAAS;EACT,SAAS,cAAc;EACvB,UAAU;EACV,UAAU;EACV,aAAa;EACb,gBAA2B,CAAC;EAC5B,mBAAmB;EACnB,GAAG;CACL;AACF;;AAGA,SAAgB,aAAa,MAAe,SAAS,KAAK;CACxD,OAAO,IAAI,SAAS,KAAK,UAAU,IAAI,GAAG;EACxC;EACA,SAAS,EAAE,gBAAgB,mBAAmB;CAChD,CAAC;AACH"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"meta-pixel.cjs","names":["mapFBEvent","getFirst"],"sources":["../../src/third-parties/meta-pixel.ts"],"sourcesContent":["import type { UpdateVisitorDTO } from '../schema/index';\nimport { type FBQ, type PixelId, mapFBEvent } from '../track/fbq';\nimport type { EventName, TrackName, TrackProperties } from '../track/types';\nimport { getFirst } from '../utils/field';\n\ndeclare global {\n interface Window {\n /** Undefined until the Meta Pixel script has loaded. */\n fbq?: FBQ['fbq'];\n }\n}\n\nconst metrics = ['CLS', 'FCP', 'FID', 'INP', 'LCP', 'TTFB'];\n\nexport function sendFBEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n event_id?: string\n) {\n if (typeof window === 'undefined' || !window.fbq) {\n console.warn('fbq has not been initialized');\n return;\n }\n if (metrics.includes(name)) return;\n if (window.location.host.includes('127.0.0.1')) return;\n if (window.location.host.includes('localhost')) return;\n\n const options = { eventID: event_id };\n const [type, fbEventName, fbEventProperties] = mapFBEvent(name, properties);\n // The two branches are identical on purpose. `fbq` is overloaded per `type` — 'track' takes a\n // standard event name with its typed properties, 'trackCustom' an arbitrary string — and what\n // `mapFBEvent` returns is a union of both shapes. Narrowing `type` is what picks a single\n // overload; collapsing the branches leaves the call matching none of them.\n if (type === 'track') {\n window.fbq(type, fbEventName, fbEventProperties, options);\n } else {\n window.fbq(type, fbEventName, fbEventProperties, options);\n }\n}\n\nexport function setFBUser(pixelId: PixelId) {\n return ({ user_id, user_data }: UpdateVisitorDTO) => {\n if (typeof window === 'undefined' || !window.fbq) {\n console.warn('fbq has not been initialized');\n return;\n }\n\n const address = getFirst(user_data?.address);\n\n window.fbq('init', pixelId, {\n em: getFirst(user_data?.email),\n fn: address?.first_name,\n ln: address?.last_name,\n ph: getFirst(user_data?.phone_number),\n external_id: user_id,\n ct: address?.city,\n st: address?.
|
|
1
|
+
{"version":3,"file":"meta-pixel.cjs","names":["mapFBEvent","getFirst"],"sources":["../../src/third-parties/meta-pixel.ts"],"sourcesContent":["import type { UpdateVisitorDTO } from '../schema/index';\nimport { type FBQ, type PixelId, mapFBEvent } from '../track/fbq';\nimport type { EventName, TrackName, TrackProperties } from '../track/types';\nimport { getFirst } from '../utils/field';\n\ndeclare global {\n interface Window {\n /** Undefined until the Meta Pixel script has loaded. */\n fbq?: FBQ['fbq'];\n }\n}\n\nconst metrics = ['CLS', 'FCP', 'FID', 'INP', 'LCP', 'TTFB'];\n\nexport function sendFBEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n event_id?: string\n) {\n if (typeof window === 'undefined' || !window.fbq) {\n console.warn('fbq has not been initialized');\n return;\n }\n if (metrics.includes(name)) return;\n if (window.location.host.includes('127.0.0.1')) return;\n if (window.location.host.includes('localhost')) return;\n\n const options = { eventID: event_id };\n const [type, fbEventName, fbEventProperties] = mapFBEvent(name, properties);\n // The two branches are identical on purpose. `fbq` is overloaded per `type` — 'track' takes a\n // standard event name with its typed properties, 'trackCustom' an arbitrary string — and what\n // `mapFBEvent` returns is a union of both shapes. Narrowing `type` is what picks a single\n // overload; collapsing the branches leaves the call matching none of them.\n if (type === 'track') {\n window.fbq(type, fbEventName, fbEventProperties, options);\n } else {\n window.fbq(type, fbEventName, fbEventProperties, options);\n }\n}\n\nexport function setFBUser(pixelId: PixelId) {\n return ({ user_id, user_data }: UpdateVisitorDTO) => {\n if (typeof window === 'undefined' || !window.fbq) {\n console.warn('fbq has not been initialized');\n return;\n }\n\n const address = getFirst(user_data?.address);\n\n window.fbq('init', pixelId, {\n em: getFirst(user_data?.email),\n fn: address?.first_name,\n ln: address?.last_name,\n ph: getFirst(user_data?.phone_number),\n external_id: user_id,\n ct: address?.city,\n // `st` is Meta's state/province field, not the street address.\n st: address?.region,\n zp: address?.postal_code,\n country: address?.country,\n });\n };\n}\n"],"mappings":";;;;AAYA,MAAM,UAAU;CAAC;CAAO;CAAO;CAAO;CAAO;CAAO;AAAM;AAE1D,SAAgB,YACd,MACA,YACA,UACA;CACA,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,KAAK;EAChD,QAAQ,KAAK,8BAA8B;EAC3C;CACF;CACA,IAAI,QAAQ,SAAS,IAAI,GAAG;CAC5B,IAAI,OAAO,SAAS,KAAK,SAAS,WAAW,GAAG;CAChD,IAAI,OAAO,SAAS,KAAK,SAAS,WAAW,GAAG;CAEhD,MAAM,UAAU,EAAE,SAAS,SAAS;CACpC,MAAM,CAAC,MAAM,aAAa,qBAAqBA,kBAAAA,WAAW,MAAM,UAAU;CAK1E,IAAI,SAAS,SACX,OAAO,IAAI,MAAM,aAAa,mBAAmB,OAAO;MAExD,OAAO,IAAI,MAAM,aAAa,mBAAmB,OAAO;AAE5D;AAEA,SAAgB,UAAU,SAAkB;CAC1C,QAAQ,EAAE,SAAS,gBAAkC;EACnD,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,KAAK;GAChD,QAAQ,KAAK,8BAA8B;GAC3C;EACF;EAEA,MAAM,UAAUC,oBAAAA,SAAS,WAAW,OAAO;EAE3C,OAAO,IAAI,QAAQ,SAAS;GAC1B,IAAIA,oBAAAA,SAAS,WAAW,KAAK;GAC7B,IAAI,SAAS;GACb,IAAI,SAAS;GACb,IAAIA,oBAAAA,SAAS,WAAW,YAAY;GACpC,aAAa;GACb,IAAI,SAAS;GAEb,IAAI,SAAS;GACb,IAAI,SAAS;GACb,SAAS,SAAS;EACpB,CAAC;CACH;AACF"}
|