@shware/analytics 10.2.1 → 11.0.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/README.md +41 -8
- package/dist/attribution/classify.cjs +11 -6
- package/dist/attribution/classify.cjs.map +1 -1
- package/dist/attribution/classify.d.cts.map +1 -1
- package/dist/attribution/classify.d.mts.map +1 -1
- package/dist/attribution/classify.mjs +12 -7
- package/dist/attribution/classify.mjs.map +1 -1
- package/dist/attribution/index.cjs +1 -0
- package/dist/attribution/index.d.cts +2 -2
- package/dist/attribution/index.d.mts +2 -2
- package/dist/attribution/index.mjs +2 -2
- package/dist/attribution/vocabulary.cjs +35 -0
- package/dist/attribution/vocabulary.cjs.map +1 -1
- package/dist/attribution/vocabulary.d.cts +13 -3
- package/dist/attribution/vocabulary.d.cts.map +1 -1
- package/dist/attribution/vocabulary.d.mts +13 -3
- package/dist/attribution/vocabulary.d.mts.map +1 -1
- package/dist/attribution/vocabulary.mjs +35 -1
- package/dist/attribution/vocabulary.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/schema/index.cjs +5 -0
- package/dist/schema/index.cjs.map +1 -1
- package/dist/schema/index.d.cts +5 -0
- package/dist/schema/index.d.cts.map +1 -1
- package/dist/schema/index.d.mts +5 -0
- package/dist/schema/index.d.mts.map +1 -1
- package/dist/schema/index.mjs +5 -0
- package/dist/schema/index.mjs.map +1 -1
- package/dist/setup/index.cjs +1 -4
- package/dist/setup/index.cjs.map +1 -1
- package/dist/setup/index.d.cts +1 -2
- package/dist/setup/index.d.cts.map +1 -1
- package/dist/setup/index.d.mts +1 -2
- package/dist/setup/index.d.mts.map +1 -1
- package/dist/setup/index.mjs +1 -4
- package/dist/setup/index.mjs.map +1 -1
- package/dist/third-parties/microsoft-uet.cjs +6 -9
- package/dist/third-parties/microsoft-uet.cjs.map +1 -1
- package/dist/third-parties/microsoft-uet.d.cts.map +1 -1
- package/dist/third-parties/microsoft-uet.d.mts.map +1 -1
- package/dist/third-parties/microsoft-uet.mjs +7 -10
- package/dist/third-parties/microsoft-uet.mjs.map +1 -1
- package/dist/track/index.cjs +11 -25
- package/dist/track/index.cjs.map +1 -1
- package/dist/track/index.d.cts +5 -5
- package/dist/track/index.d.cts.map +1 -1
- package/dist/track/index.d.mts +5 -5
- package/dist/track/index.d.mts.map +1 -1
- package/dist/track/index.mjs +12 -26
- package/dist/track/index.mjs.map +1 -1
- package/dist/visitor/index.cjs +19 -52
- package/dist/visitor/index.cjs.map +1 -1
- package/dist/visitor/index.d.cts +10 -1
- package/dist/visitor/index.d.cts.map +1 -1
- package/dist/visitor/index.d.mts +10 -1
- package/dist/visitor/index.d.mts.map +1 -1
- package/dist/visitor/index.mjs +20 -53
- package/dist/visitor/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/setup/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../../src/setup/index.ts"],"sourcesContent":["import type { Environment, Platform, ThirdPartyTracker, TrackTags } from '../track/types';\nimport type { ThirdPartyUserSetter
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../../src/setup/index.ts"],"sourcesContent":["import type { Environment, Platform, ThirdPartyTracker, TrackTags } from '../track/types';\nimport type { ThirdPartyUserSetter } from '../visitor/types';\n\nexport interface Storage {\n getItem: (key: string) => string | null;\n setItem: (key: string, value: string) => void;\n}\n\nexport interface Options {\n release: string;\n storage: Storage;\n endpoint: string;\n platform: Platform;\n environment: Environment;\n getTags: () => TrackTags | Promise<TrackTags>;\n getDeviceId: () => string | Promise<string>;\n getHeaders?: () => Record<string, string> | Promise<Record<string, string>>;\n thirdPartyTrackers?: ThirdPartyTracker[];\n thirdPartyUserSetters?: ThirdPartyUserSetter[];\n}\n\ninterface Config {\n release: string;\n endpoint: string;\n storage: Storage;\n platform: Platform;\n environment: Environment;\n getTags: () => TrackTags | Promise<TrackTags>;\n getDeviceId: () => string | Promise<string>;\n getHeaders: () => Record<string, string> | Promise<Record<string, string>>;\n thirdPartyTrackers: ThirdPartyTracker[];\n thirdPartyUserSetters: ThirdPartyUserSetter[];\n}\n\ninterface Cache {\n tags: TrackTags | null;\n}\n\nexport const cache: Cache = {\n tags: null,\n};\n\n// oxlint-disable typescript/no-non-null-assertion\nexport const config: Config = {\n endpoint: '',\n release: '0.0.0',\n storage: null!,\n platform: null!,\n environment: null!,\n getTags: null!,\n getDeviceId: null!,\n getHeaders: null!,\n thirdPartyTrackers: [],\n thirdPartyUserSetters: [],\n};\n// oxlint-enable typescript/no-non-null-assertion\n\nexport function setupAnalytics(init: Options) {\n config.release = init.release;\n config.storage = init.storage;\n config.platform = init.platform;\n config.environment = init.environment;\n config.endpoint = init.endpoint.endsWith('/') ? init.endpoint.slice(0, -1) : init.endpoint;\n config.getTags = init.getTags;\n config.getDeviceId = init.getDeviceId;\n config.getHeaders = async () => ({\n 'Content-Type': 'application/json',\n ...(await init.getHeaders?.()),\n });\n config.thirdPartyTrackers = init.thirdPartyTrackers ?? [];\n config.thirdPartyUserSetters = init.thirdPartyUserSetters ?? [];\n}\n"],"mappings":";;AAsCA,MAAa,QAAe,EAC1B,MAAM,KACR;AAGA,MAAa,SAAiB;CAC5B,UAAU;CACV,SAAS;CACT,SAAS;CACT,UAAU;CACV,aAAa;CACb,SAAS;CACT,aAAa;CACb,YAAY;CACZ,oBAAoB,CAAC;CACrB,uBAAuB,CAAC;AAC1B;AAGA,SAAgB,eAAe,MAAe;CAC5C,OAAO,UAAU,KAAK;CACtB,OAAO,UAAU,KAAK;CACtB,OAAO,WAAW,KAAK;CACvB,OAAO,cAAc,KAAK;CAC1B,OAAO,WAAW,KAAK,SAAS,SAAS,GAAG,IAAI,KAAK,SAAS,MAAM,GAAG,EAAE,IAAI,KAAK;CAClF,OAAO,UAAU,KAAK;CACtB,OAAO,cAAc,KAAK;CAC1B,OAAO,aAAa,aAAa;EAC/B,gBAAgB;EAChB,GAAI,MAAM,KAAK,aAAa;CAC9B;CACA,OAAO,qBAAqB,KAAK,sBAAsB,CAAC;CACxD,OAAO,wBAAwB,KAAK,yBAAyB,CAAC;AAChE"}
|
package/dist/setup/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Environment, Platform, ThirdPartyTracker, TrackTags } from "../track/types.cjs";
|
|
2
|
-
import { ThirdPartyUserSetter
|
|
2
|
+
import { ThirdPartyUserSetter } from "../visitor/types.cjs";
|
|
3
3
|
//#region src/setup/index.d.ts
|
|
4
4
|
export interface Storage {
|
|
5
5
|
getItem: (key: string) => string | null;
|
|
@@ -31,7 +31,6 @@ interface Config {
|
|
|
31
31
|
}
|
|
32
32
|
interface Cache {
|
|
33
33
|
tags: TrackTags | null;
|
|
34
|
-
visitor: Visitor | null;
|
|
35
34
|
}
|
|
36
35
|
export declare const cache: Cache;
|
|
37
36
|
export declare const config: Config;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/setup/index.ts"],"mappings":";;;iBAGiB;EACf,UAAU;EACV,UAAU,aAAa;;iBAGR;EACf;EACA,SAAS;EACT;EACA,UAAU;EACV,aAAa;EACb,eAAe,YAAY,QAAQ;EACnC,4BAA4B;EAC5B,mBAAmB,yBAAyB,QAAQ;EACpD,qBAAqB;EACrB,wBAAwB;;UAGhB;EACR;EACA;EACA,SAAS;EACT,UAAU;EACV,aAAa;EACb,eAAe,YAAY,QAAQ;EACnC,4BAA4B;EAC5B,kBAAkB,yBAAyB,QAAQ;EACnD,oBAAoB;EACpB,uBAAuB;;UAGf;EACR,MAAM
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/setup/index.ts"],"mappings":";;;iBAGiB;EACf,UAAU;EACV,UAAU,aAAa;;iBAGR;EACf;EACA,SAAS;EACT;EACA,UAAU;EACV,aAAa;EACb,eAAe,YAAY,QAAQ;EACnC,4BAA4B;EAC5B,mBAAmB,yBAAyB,QAAQ;EACpD,qBAAqB;EACrB,wBAAwB;;UAGhB;EACR;EACA;EACA,SAAS;EACT,UAAU;EACV,aAAa;EACb,eAAe,YAAY,QAAQ;EACnC,4BAA4B;EAC5B,kBAAkB,yBAAyB,QAAQ;EACnD,oBAAoB;EACpB,uBAAuB;;UAGf;EACR,MAAM;;qBAGK,OAAO;qBAKP,QAAQ;wBAcL,eAAe,MAAM"}
|
package/dist/setup/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Environment, Platform, ThirdPartyTracker, TrackTags } from "../track/types.mjs";
|
|
2
|
-
import { ThirdPartyUserSetter
|
|
2
|
+
import { ThirdPartyUserSetter } from "../visitor/types.mjs";
|
|
3
3
|
//#region src/setup/index.d.ts
|
|
4
4
|
export interface Storage {
|
|
5
5
|
getItem: (key: string) => string | null;
|
|
@@ -31,7 +31,6 @@ interface Config {
|
|
|
31
31
|
}
|
|
32
32
|
interface Cache {
|
|
33
33
|
tags: TrackTags | null;
|
|
34
|
-
visitor: Visitor | null;
|
|
35
34
|
}
|
|
36
35
|
export declare const cache: Cache;
|
|
37
36
|
export declare const config: Config;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/setup/index.ts"],"mappings":";;;iBAGiB;EACf,UAAU;EACV,UAAU,aAAa;;iBAGR;EACf;EACA,SAAS;EACT;EACA,UAAU;EACV,aAAa;EACb,eAAe,YAAY,QAAQ;EACnC,4BAA4B;EAC5B,mBAAmB,yBAAyB,QAAQ;EACpD,qBAAqB;EACrB,wBAAwB;;UAGhB;EACR;EACA;EACA,SAAS;EACT,UAAU;EACV,aAAa;EACb,eAAe,YAAY,QAAQ;EACnC,4BAA4B;EAC5B,kBAAkB,yBAAyB,QAAQ;EACnD,oBAAoB;EACpB,uBAAuB;;UAGf;EACR,MAAM
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/setup/index.ts"],"mappings":";;;iBAGiB;EACf,UAAU;EACV,UAAU,aAAa;;iBAGR;EACf;EACA,SAAS;EACT;EACA,UAAU;EACV,aAAa;EACb,eAAe,YAAY,QAAQ;EACnC,4BAA4B;EAC5B,mBAAmB,yBAAyB,QAAQ;EACpD,qBAAqB;EACrB,wBAAwB;;UAGhB;EACR;EACA;EACA,SAAS;EACT,UAAU;EACV,aAAa;EACb,eAAe,YAAY,QAAQ;EACnC,4BAA4B;EAC5B,kBAAkB,yBAAyB,QAAQ;EACnD,oBAAoB;EACpB,uBAAuB;;UAGf;EACR,MAAM;;qBAGK,OAAO;qBAKP,QAAQ;wBAcL,eAAe,MAAM"}
|
package/dist/setup/index.mjs
CHANGED
package/dist/setup/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/setup/index.ts"],"sourcesContent":["import type { Environment, Platform, ThirdPartyTracker, TrackTags } from '../track/types';\nimport type { ThirdPartyUserSetter
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/setup/index.ts"],"sourcesContent":["import type { Environment, Platform, ThirdPartyTracker, TrackTags } from '../track/types';\nimport type { ThirdPartyUserSetter } from '../visitor/types';\n\nexport interface Storage {\n getItem: (key: string) => string | null;\n setItem: (key: string, value: string) => void;\n}\n\nexport interface Options {\n release: string;\n storage: Storage;\n endpoint: string;\n platform: Platform;\n environment: Environment;\n getTags: () => TrackTags | Promise<TrackTags>;\n getDeviceId: () => string | Promise<string>;\n getHeaders?: () => Record<string, string> | Promise<Record<string, string>>;\n thirdPartyTrackers?: ThirdPartyTracker[];\n thirdPartyUserSetters?: ThirdPartyUserSetter[];\n}\n\ninterface Config {\n release: string;\n endpoint: string;\n storage: Storage;\n platform: Platform;\n environment: Environment;\n getTags: () => TrackTags | Promise<TrackTags>;\n getDeviceId: () => string | Promise<string>;\n getHeaders: () => Record<string, string> | Promise<Record<string, string>>;\n thirdPartyTrackers: ThirdPartyTracker[];\n thirdPartyUserSetters: ThirdPartyUserSetter[];\n}\n\ninterface Cache {\n tags: TrackTags | null;\n}\n\nexport const cache: Cache = {\n tags: null,\n};\n\n// oxlint-disable typescript/no-non-null-assertion\nexport const config: Config = {\n endpoint: '',\n release: '0.0.0',\n storage: null!,\n platform: null!,\n environment: null!,\n getTags: null!,\n getDeviceId: null!,\n getHeaders: null!,\n thirdPartyTrackers: [],\n thirdPartyUserSetters: [],\n};\n// oxlint-enable typescript/no-non-null-assertion\n\nexport function setupAnalytics(init: Options) {\n config.release = init.release;\n config.storage = init.storage;\n config.platform = init.platform;\n config.environment = init.environment;\n config.endpoint = init.endpoint.endsWith('/') ? init.endpoint.slice(0, -1) : init.endpoint;\n config.getTags = init.getTags;\n config.getDeviceId = init.getDeviceId;\n config.getHeaders = async () => ({\n 'Content-Type': 'application/json',\n ...(await init.getHeaders?.()),\n });\n config.thirdPartyTrackers = init.thirdPartyTrackers ?? [];\n config.thirdPartyUserSetters = init.thirdPartyUserSetters ?? [];\n}\n"],"mappings":";AAsCA,MAAa,QAAe,EAC1B,MAAM,KACR;AAGA,MAAa,SAAiB;CAC5B,UAAU;CACV,SAAS;CACT,SAAS;CACT,UAAU;CACV,aAAa;CACb,SAAS;CACT,aAAa;CACb,YAAY;CACZ,oBAAoB,CAAC;CACrB,uBAAuB,CAAC;AAC1B;AAGA,SAAgB,eAAe,MAAe;CAC5C,OAAO,UAAU,KAAK;CACtB,OAAO,UAAU,KAAK;CACtB,OAAO,WAAW,KAAK;CACvB,OAAO,cAAc,KAAK;CAC1B,OAAO,WAAW,KAAK,SAAS,SAAS,GAAG,IAAI,KAAK,SAAS,MAAM,GAAG,EAAE,IAAI,KAAK;CAClF,OAAO,UAAU,KAAK;CACtB,OAAO,cAAc,KAAK;CAC1B,OAAO,aAAa,aAAa;EAC/B,gBAAgB;EAChB,GAAI,MAAM,KAAK,aAAa;CAC9B;CACA,OAAO,qBAAqB,KAAK,sBAAsB,CAAC;CACxD,OAAO,wBAAwB,KAAK,yBAAyB,CAAC;AAChE"}
|
|
@@ -86,7 +86,7 @@ function setUETConsent(mode, consent) {
|
|
|
86
86
|
* browser context of the sync, which a server-side call cannot carry.
|
|
87
87
|
*
|
|
88
88
|
* `customerId` is the Microsoft Advertising customer id (`cid` in the UI's URLs) — not the UET
|
|
89
|
-
* tag id. `visitorId` is the SDK visitor id (`
|
|
89
|
+
* tag id. `visitorId` is the SDK visitor id (`visitorId()`), which is what
|
|
90
90
|
* `sendMicrosoftEvents` sends as `anonymousId`; Microsoft requires the two to be equal.
|
|
91
91
|
* `userId` is the raw signed-in user id: it is SHA-256 hashed here, exactly as the server sender
|
|
92
92
|
* hashes it into `externalId`, so the pair matches as well. Hashing is asynchronous, so the
|
|
@@ -110,14 +110,11 @@ function configureUET(options) {
|
|
|
110
110
|
*/
|
|
111
111
|
async function syncUETVisitor(userId) {
|
|
112
112
|
if (!uetCustomerId) return;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
userId
|
|
119
|
-
});
|
|
120
|
-
} catch {}
|
|
113
|
+
sendUETIdSync({
|
|
114
|
+
customerId: uetCustomerId,
|
|
115
|
+
visitorId: require_visitor_index.visitorId(),
|
|
116
|
+
userId
|
|
117
|
+
});
|
|
121
118
|
}
|
|
122
119
|
function sendUETIdSync(options) {
|
|
123
120
|
if (typeof window === "undefined" || typeof Image === "undefined") return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"microsoft-uet.cjs","names":["mapUETEvent","getFirst","
|
|
1
|
+
{"version":3,"file":"microsoft-uet.cjs","names":["mapUETEvent","getFirst","visitorId"],"sources":["../../src/third-parties/microsoft-uet.ts"],"sourcesContent":["import type { UpdateVisitorDTO } from '../schema/index';\nimport type { EventName, TrackName, TrackProperties } from '../track/types';\nimport { type UETConsent, type UETQ, mapUETEvent } from '../track/uetq';\nimport { getFirst } from '../utils/field';\nimport { sha256 } from '../utils/sha256';\nimport { visitorId } from '../visitor/index';\n\ndeclare global {\n interface Window {\n /**\n * Defined by the UET snippet before `bat.js` loads (as a plain array), so pushes made early\n * are queued rather than lost. Undefined only where the snippet was never installed.\n */\n uetq?: UETQ;\n }\n}\n\nconst metrics = ['CLS', 'FCP', 'FID', 'INP', 'LCP', 'TTFB'];\n\n/** Drop `undefined` fields: bat.js validates every key it receives, and `undefined` is a value. */\n// oxlint-disable-next-line @typescript-eslint/no-explicit-any\nfunction clean(value: unknown): any {\n return JSON.parse(JSON.stringify(value));\n}\n\nfunction isLocal(): boolean {\n return window.location.host.includes('127.0.0.1') || window.location.host.includes('localhost');\n}\n\n/**\n * Forward an internal track event to the Microsoft Advertising UET tag.\n *\n * `eventId` is reused as UET's `event_id` so the browser event deduplicates against the same\n * conversion sent through the Conversions API (`sendMicrosoftEvents`). The action name is the\n * internal name on both channels — see `mapUETEvent`.\n *\n * `page_view` is not forwarded: bat.js turns that action into its page-load beacon, which the\n * snippet already fires on load and, with `enableAutoSpaTracking: true` (the setting in\n * Microsoft's generated snippet), on every history change — forwarding the SDK's own page views\n * would report each page twice. The tag owns page loads; this tracker owns everything else.\n */\nexport function sendUETEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n eventId?: string\n) {\n if (typeof window === 'undefined' || !window.uetq) {\n console.warn('uetq has not been initialized');\n return;\n }\n if (metrics.includes(name) || name === 'page_view') return;\n if (isLocal()) return;\n\n const [action, params] = mapUETEvent(name, properties, eventId);\n window.uetq.push('event', action, clean(params));\n}\n\n/**\n * Enhanced conversions: hand the tag the visitor's email and phone so Microsoft can match a\n * conversion to an ad click without a third-party cookie. A `set` applies to every event fired\n * from the page afterwards, `pageLoad` included, and the tag fires a `pid` beacon on its own\n * when the page load already went out. Values are sent raw — the tag normalizes and SHA-256\n * hashes them in the browser (bat.js `validatePid`) before anything leaves the page.\n *\n * A setter itself, like `setGAUser`, rather than a factory like `setFBUser(pixelId)`: UET is\n * bound to its tag by the snippet and `set` addresses whichever tag drains `uetq`, so there is\n * nothing to bind first.\n */\nexport function setUETUser({ user_id, user_data }: UpdateVisitorDTO) {\n if (typeof window === 'undefined' || !window.uetq) {\n console.warn('uetq has not been initialized');\n return;\n }\n\n // A signed-in user is also the moment the ID Sync pixel can carry a `UID` — see `syncUETVisitor`.\n if (user_id) void syncUETVisitor(user_id);\n\n const em = getFirst(user_data?.email);\n const ph = getFirst(user_data?.phone_number);\n if (!em && !ph) return;\n\n window.uetq.push('set', { pid: clean({ em, ph }) });\n}\n\n/**\n * UET consent mode. Push `default` before the tag loads (the tag processes `consent` early, ahead\n * of the queued events) and `update` when the visitor decides. With `ad_storage: 'denied'` the\n * tag neither writes its cookies nor reads the `msclkid` from the URL.\n * https://help.ads.microsoft.com/#apex/ads/en/60119/1\n */\nexport function setUETConsent(mode: 'default' | 'update', consent: UETConsent) {\n if (typeof window === 'undefined') return;\n window.uetq ??= [] as unknown as UETQ;\n window.uetq.push('consent', mode, consent);\n}\n\n/**\n * The Conversions API's client-side ID Sync pixel: ties the visitor id sent server-side as\n * `anonymousId` to Microsoft's own identifier, so server events can be attributed across\n * contexts (view-through, audiences). Microsoft requires it for remarketing built from CAPI\n * events and recommends it for measurement quality; fire it at least once per session, ideally\n * on the first page view. A pixel rather than a fetch on purpose: Microsoft must observe the\n * browser context of the sync, which a server-side call cannot carry.\n *\n * `customerId` is the Microsoft Advertising customer id (`cid` in the UI's URLs) — not the UET\n * tag id. `visitorId` is the SDK visitor id (`visitorId()`), which is what\n * `sendMicrosoftEvents` sends as `anonymousId`; Microsoft requires the two to be equal.\n * `userId` is the raw signed-in user id: it is SHA-256 hashed here, exactly as the server sender\n * hashes it into `externalId`, so the pair matches as well. Hashing is asynchronous, so the\n * pixel fires once the digest resolves; where SubtleCrypto is unavailable (an insecure context)\n * the pixel goes out without `UID` rather than with a raw id.\n * https://learn.microsoft.com/en-us/advertising/guides/uet-conversion-api-integration#id-sync-and-why-it-matters\n */\n/**\n * The Microsoft Advertising customer id the ID Sync pixel reports to, set once by the framework\n * `Analytics` component's `uetCustomerId` prop (or `configureUET`). Unset, nothing syncs.\n */\nlet uetCustomerId: string | undefined;\n\nexport function configureUET(options: { customerId?: string | number }) {\n uetCustomerId = options.customerId === undefined ? undefined : String(options.customerId);\n}\n\n/**\n * Sync the SDK visitor — the `anonymousId` of the events the server sends to the Conversions\n * API — with Microsoft, plus the user's `UID` when one is known. The `Analytics` components run\n * it on mount so every visit syncs at least once, anonymous visitors included (they are who\n * remarketing audiences are built from), and `setUETUser` runs it again on sign-in.\n */\nexport async function syncUETVisitor(userId?: string): Promise<void> {\n if (!uetCustomerId) return;\n sendUETIdSync({ customerId: uetCustomerId, visitorId: visitorId(), userId });\n}\n\nexport function sendUETIdSync(options: {\n customerId: string | number;\n visitorId: string;\n /** The signed-in user's id, raw; hashed before it leaves the page. */\n userId?: string;\n}) {\n if (typeof window === 'undefined' || typeof Image === 'undefined') return;\n if (isLocal()) return;\n\n const fire = (uid?: string) => {\n const params = new URLSearchParams({\n Red3: `BACID_${options.customerId}`,\n VID: options.visitorId,\n });\n if (uid) params.set('UID', uid);\n new Image().src = `https://c.bing.com/c.gif?${params.toString()}`;\n };\n\n if (!options.userId) {\n fire();\n return;\n }\n sha256(options.userId).then(fire, () => fire());\n}\n"],"mappings":";;;;;;AAiBA,MAAM,UAAU;CAAC;CAAO;CAAO;CAAO;CAAO;CAAO;AAAM;;AAI1D,SAAS,MAAM,OAAqB;CAClC,OAAO,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AACzC;AAEA,SAAS,UAAmB;CAC1B,OAAO,OAAO,SAAS,KAAK,SAAS,WAAW,KAAK,OAAO,SAAS,KAAK,SAAS,WAAW;AAChG;;;;;;;;;;;;;AAcA,SAAgB,aACd,MACA,YACA,SACA;CACA,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,MAAM;EACjD,QAAQ,KAAK,+BAA+B;EAC5C;CACF;CACA,IAAI,QAAQ,SAAS,IAAI,KAAK,SAAS,aAAa;CACpD,IAAI,QAAQ,GAAG;CAEf,MAAM,CAAC,QAAQ,UAAUA,mBAAAA,YAAY,MAAM,YAAY,OAAO;CAC9D,OAAO,KAAK,KAAK,SAAS,QAAQ,MAAM,MAAM,CAAC;AACjD;;;;;;;;;;;;AAaA,SAAgB,WAAW,EAAE,SAAS,aAA+B;CACnE,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,MAAM;EACjD,QAAQ,KAAK,+BAA+B;EAC5C;CACF;CAGA,IAAI,SAAS,eAAoB,OAAO;CAExC,MAAM,KAAKC,oBAAAA,SAAS,WAAW,KAAK;CACpC,MAAM,KAAKA,oBAAAA,SAAS,WAAW,YAAY;CAC3C,IAAI,CAAC,MAAM,CAAC,IAAI;CAEhB,OAAO,KAAK,KAAK,OAAO,EAAE,KAAK,MAAM;EAAE;EAAI;CAAG,CAAC,EAAE,CAAC;AACpD;;;;;;;AAQA,SAAgB,cAAc,MAA4B,SAAqB;CAC7E,IAAI,OAAO,WAAW,aAAa;CACnC,OAAO,SAAS,CAAC;CACjB,OAAO,KAAK,KAAK,WAAW,MAAM,OAAO;AAC3C;;;;;;;;;;;;;;;;;;;;;;AAuBA,IAAI;AAEJ,SAAgB,aAAa,SAA2C;CACtE,gBAAgB,QAAQ,eAAe,KAAA,IAAY,KAAA,IAAY,OAAO,QAAQ,UAAU;AAC1F;;;;;;;AAQA,eAAsB,eAAe,QAAgC;CACnE,IAAI,CAAC,eAAe;CACpB,cAAc;EAAE,YAAY;EAAe,WAAWC,sBAAAA,UAAU;EAAG;CAAO,CAAC;AAC7E;AAEA,SAAgB,cAAc,SAK3B;CACD,IAAI,OAAO,WAAW,eAAe,OAAO,UAAU,aAAa;CACnE,IAAI,QAAQ,GAAG;CAEf,MAAM,QAAQ,QAAiB;EAC7B,MAAM,SAAS,IAAI,gBAAgB;GACjC,MAAM,SAAS,QAAQ;GACvB,KAAK,QAAQ;EACf,CAAC;EACD,IAAI,KAAK,OAAO,IAAI,OAAO,GAAG;EAC9B,IAAI,MAAM,CAAC,CAAC,MAAM,4BAA4B,OAAO,SAAS;CAChE;CAEA,IAAI,CAAC,QAAQ,QAAQ;EACnB,KAAK;EACL;CACF;CACA,qBAAA,OAAO,QAAQ,MAAM,CAAC,CAAC,KAAK,YAAY,KAAK,CAAC;AAChD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"microsoft-uet.d.cts","names":[],"sources":["../../src/third-parties/microsoft-uet.ts"],"mappings":";;;;QAOQ;YACI;;;;;IAKR,OAAO;;;;;;;;;;;;;;;wBA4BK,aAAa,UAAU,WACrC,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B;;;;;;;;;;;;wBAwBc,aAAa,SAAS,aAAa;;;;;;;wBAsBnC,cAAc,4BAA4B,SAAS;wBA6BnD,aAAa;EAAW;;;;;;;;wBAUlB,eAAe,kBAAkB;
|
|
1
|
+
{"version":3,"file":"microsoft-uet.d.cts","names":[],"sources":["../../src/third-parties/microsoft-uet.ts"],"mappings":";;;;QAOQ;YACI;;;;;IAKR,OAAO;;;;;;;;;;;;;;;wBA4BK,aAAa,UAAU,WACrC,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B;;;;;;;;;;;;wBAwBc,aAAa,SAAS,aAAa;;;;;;;wBAsBnC,cAAc,4BAA4B,SAAS;wBA6BnD,aAAa;EAAW;;;;;;;;wBAUlB,eAAe,kBAAkB;wBAKvC,cAAc;EAC5B;EACA;;EAEA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"microsoft-uet.d.mts","names":[],"sources":["../../src/third-parties/microsoft-uet.ts"],"mappings":";;;;QAOQ;YACI;;;;;IAKR,OAAO;;;;;;;;;;;;;;;wBA4BK,aAAa,UAAU,WACrC,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B;;;;;;;;;;;;wBAwBc,aAAa,SAAS,aAAa;;;;;;;wBAsBnC,cAAc,4BAA4B,SAAS;wBA6BnD,aAAa;EAAW;;;;;;;;wBAUlB,eAAe,kBAAkB;
|
|
1
|
+
{"version":3,"file":"microsoft-uet.d.mts","names":[],"sources":["../../src/third-parties/microsoft-uet.ts"],"mappings":";;;;QAOQ;YACI;;;;;IAKR,OAAO;;;;;;;;;;;;;;;wBA4BK,aAAa,UAAU,WACrC,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B;;;;;;;;;;;;wBAwBc,aAAa,SAAS,aAAa;;;;;;;wBAsBnC,cAAc,4BAA4B,SAAS;wBA6BnD,aAAa;EAAW;;;;;;;;wBAUlB,eAAe,kBAAkB;wBAKvC,cAAc;EAC5B;EACA;;EAEA"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { visitorId } from "../visitor/index.mjs";
|
|
2
2
|
import { mapUETEvent } from "../track/uetq.mjs";
|
|
3
3
|
import { getFirst } from "../utils/field.mjs";
|
|
4
4
|
import { sha256 } from "../utils/sha256.mjs";
|
|
@@ -85,7 +85,7 @@ function setUETConsent(mode, consent) {
|
|
|
85
85
|
* browser context of the sync, which a server-side call cannot carry.
|
|
86
86
|
*
|
|
87
87
|
* `customerId` is the Microsoft Advertising customer id (`cid` in the UI's URLs) — not the UET
|
|
88
|
-
* tag id. `visitorId` is the SDK visitor id (`
|
|
88
|
+
* tag id. `visitorId` is the SDK visitor id (`visitorId()`), which is what
|
|
89
89
|
* `sendMicrosoftEvents` sends as `anonymousId`; Microsoft requires the two to be equal.
|
|
90
90
|
* `userId` is the raw signed-in user id: it is SHA-256 hashed here, exactly as the server sender
|
|
91
91
|
* hashes it into `externalId`, so the pair matches as well. Hashing is asynchronous, so the
|
|
@@ -109,14 +109,11 @@ function configureUET(options) {
|
|
|
109
109
|
*/
|
|
110
110
|
async function syncUETVisitor(userId) {
|
|
111
111
|
if (!uetCustomerId) return;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
userId
|
|
118
|
-
});
|
|
119
|
-
} catch {}
|
|
112
|
+
sendUETIdSync({
|
|
113
|
+
customerId: uetCustomerId,
|
|
114
|
+
visitorId: visitorId(),
|
|
115
|
+
userId
|
|
116
|
+
});
|
|
120
117
|
}
|
|
121
118
|
function sendUETIdSync(options) {
|
|
122
119
|
if (typeof window === "undefined" || typeof Image === "undefined") return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"microsoft-uet.mjs","names":[],"sources":["../../src/third-parties/microsoft-uet.ts"],"sourcesContent":["import type { UpdateVisitorDTO } from '../schema/index';\nimport type { EventName, TrackName, TrackProperties } from '../track/types';\nimport { type UETConsent, type UETQ, mapUETEvent } from '../track/uetq';\nimport { getFirst } from '../utils/field';\nimport { sha256 } from '../utils/sha256';\nimport {
|
|
1
|
+
{"version":3,"file":"microsoft-uet.mjs","names":[],"sources":["../../src/third-parties/microsoft-uet.ts"],"sourcesContent":["import type { UpdateVisitorDTO } from '../schema/index';\nimport type { EventName, TrackName, TrackProperties } from '../track/types';\nimport { type UETConsent, type UETQ, mapUETEvent } from '../track/uetq';\nimport { getFirst } from '../utils/field';\nimport { sha256 } from '../utils/sha256';\nimport { visitorId } from '../visitor/index';\n\ndeclare global {\n interface Window {\n /**\n * Defined by the UET snippet before `bat.js` loads (as a plain array), so pushes made early\n * are queued rather than lost. Undefined only where the snippet was never installed.\n */\n uetq?: UETQ;\n }\n}\n\nconst metrics = ['CLS', 'FCP', 'FID', 'INP', 'LCP', 'TTFB'];\n\n/** Drop `undefined` fields: bat.js validates every key it receives, and `undefined` is a value. */\n// oxlint-disable-next-line @typescript-eslint/no-explicit-any\nfunction clean(value: unknown): any {\n return JSON.parse(JSON.stringify(value));\n}\n\nfunction isLocal(): boolean {\n return window.location.host.includes('127.0.0.1') || window.location.host.includes('localhost');\n}\n\n/**\n * Forward an internal track event to the Microsoft Advertising UET tag.\n *\n * `eventId` is reused as UET's `event_id` so the browser event deduplicates against the same\n * conversion sent through the Conversions API (`sendMicrosoftEvents`). The action name is the\n * internal name on both channels — see `mapUETEvent`.\n *\n * `page_view` is not forwarded: bat.js turns that action into its page-load beacon, which the\n * snippet already fires on load and, with `enableAutoSpaTracking: true` (the setting in\n * Microsoft's generated snippet), on every history change — forwarding the SDK's own page views\n * would report each page twice. The tag owns page loads; this tracker owns everything else.\n */\nexport function sendUETEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n eventId?: string\n) {\n if (typeof window === 'undefined' || !window.uetq) {\n console.warn('uetq has not been initialized');\n return;\n }\n if (metrics.includes(name) || name === 'page_view') return;\n if (isLocal()) return;\n\n const [action, params] = mapUETEvent(name, properties, eventId);\n window.uetq.push('event', action, clean(params));\n}\n\n/**\n * Enhanced conversions: hand the tag the visitor's email and phone so Microsoft can match a\n * conversion to an ad click without a third-party cookie. A `set` applies to every event fired\n * from the page afterwards, `pageLoad` included, and the tag fires a `pid` beacon on its own\n * when the page load already went out. Values are sent raw — the tag normalizes and SHA-256\n * hashes them in the browser (bat.js `validatePid`) before anything leaves the page.\n *\n * A setter itself, like `setGAUser`, rather than a factory like `setFBUser(pixelId)`: UET is\n * bound to its tag by the snippet and `set` addresses whichever tag drains `uetq`, so there is\n * nothing to bind first.\n */\nexport function setUETUser({ user_id, user_data }: UpdateVisitorDTO) {\n if (typeof window === 'undefined' || !window.uetq) {\n console.warn('uetq has not been initialized');\n return;\n }\n\n // A signed-in user is also the moment the ID Sync pixel can carry a `UID` — see `syncUETVisitor`.\n if (user_id) void syncUETVisitor(user_id);\n\n const em = getFirst(user_data?.email);\n const ph = getFirst(user_data?.phone_number);\n if (!em && !ph) return;\n\n window.uetq.push('set', { pid: clean({ em, ph }) });\n}\n\n/**\n * UET consent mode. Push `default` before the tag loads (the tag processes `consent` early, ahead\n * of the queued events) and `update` when the visitor decides. With `ad_storage: 'denied'` the\n * tag neither writes its cookies nor reads the `msclkid` from the URL.\n * https://help.ads.microsoft.com/#apex/ads/en/60119/1\n */\nexport function setUETConsent(mode: 'default' | 'update', consent: UETConsent) {\n if (typeof window === 'undefined') return;\n window.uetq ??= [] as unknown as UETQ;\n window.uetq.push('consent', mode, consent);\n}\n\n/**\n * The Conversions API's client-side ID Sync pixel: ties the visitor id sent server-side as\n * `anonymousId` to Microsoft's own identifier, so server events can be attributed across\n * contexts (view-through, audiences). Microsoft requires it for remarketing built from CAPI\n * events and recommends it for measurement quality; fire it at least once per session, ideally\n * on the first page view. A pixel rather than a fetch on purpose: Microsoft must observe the\n * browser context of the sync, which a server-side call cannot carry.\n *\n * `customerId` is the Microsoft Advertising customer id (`cid` in the UI's URLs) — not the UET\n * tag id. `visitorId` is the SDK visitor id (`visitorId()`), which is what\n * `sendMicrosoftEvents` sends as `anonymousId`; Microsoft requires the two to be equal.\n * `userId` is the raw signed-in user id: it is SHA-256 hashed here, exactly as the server sender\n * hashes it into `externalId`, so the pair matches as well. Hashing is asynchronous, so the\n * pixel fires once the digest resolves; where SubtleCrypto is unavailable (an insecure context)\n * the pixel goes out without `UID` rather than with a raw id.\n * https://learn.microsoft.com/en-us/advertising/guides/uet-conversion-api-integration#id-sync-and-why-it-matters\n */\n/**\n * The Microsoft Advertising customer id the ID Sync pixel reports to, set once by the framework\n * `Analytics` component's `uetCustomerId` prop (or `configureUET`). Unset, nothing syncs.\n */\nlet uetCustomerId: string | undefined;\n\nexport function configureUET(options: { customerId?: string | number }) {\n uetCustomerId = options.customerId === undefined ? undefined : String(options.customerId);\n}\n\n/**\n * Sync the SDK visitor — the `anonymousId` of the events the server sends to the Conversions\n * API — with Microsoft, plus the user's `UID` when one is known. The `Analytics` components run\n * it on mount so every visit syncs at least once, anonymous visitors included (they are who\n * remarketing audiences are built from), and `setUETUser` runs it again on sign-in.\n */\nexport async function syncUETVisitor(userId?: string): Promise<void> {\n if (!uetCustomerId) return;\n sendUETIdSync({ customerId: uetCustomerId, visitorId: visitorId(), userId });\n}\n\nexport function sendUETIdSync(options: {\n customerId: string | number;\n visitorId: string;\n /** The signed-in user's id, raw; hashed before it leaves the page. */\n userId?: string;\n}) {\n if (typeof window === 'undefined' || typeof Image === 'undefined') return;\n if (isLocal()) return;\n\n const fire = (uid?: string) => {\n const params = new URLSearchParams({\n Red3: `BACID_${options.customerId}`,\n VID: options.visitorId,\n });\n if (uid) params.set('UID', uid);\n new Image().src = `https://c.bing.com/c.gif?${params.toString()}`;\n };\n\n if (!options.userId) {\n fire();\n return;\n }\n sha256(options.userId).then(fire, () => fire());\n}\n"],"mappings":";;;;;AAiBA,MAAM,UAAU;CAAC;CAAO;CAAO;CAAO;CAAO;CAAO;AAAM;;AAI1D,SAAS,MAAM,OAAqB;CAClC,OAAO,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AACzC;AAEA,SAAS,UAAmB;CAC1B,OAAO,OAAO,SAAS,KAAK,SAAS,WAAW,KAAK,OAAO,SAAS,KAAK,SAAS,WAAW;AAChG;;;;;;;;;;;;;AAcA,SAAgB,aACd,MACA,YACA,SACA;CACA,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,MAAM;EACjD,QAAQ,KAAK,+BAA+B;EAC5C;CACF;CACA,IAAI,QAAQ,SAAS,IAAI,KAAK,SAAS,aAAa;CACpD,IAAI,QAAQ,GAAG;CAEf,MAAM,CAAC,QAAQ,UAAU,YAAY,MAAM,YAAY,OAAO;CAC9D,OAAO,KAAK,KAAK,SAAS,QAAQ,MAAM,MAAM,CAAC;AACjD;;;;;;;;;;;;AAaA,SAAgB,WAAW,EAAE,SAAS,aAA+B;CACnE,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,MAAM;EACjD,QAAQ,KAAK,+BAA+B;EAC5C;CACF;CAGA,IAAI,SAAS,eAAoB,OAAO;CAExC,MAAM,KAAK,SAAS,WAAW,KAAK;CACpC,MAAM,KAAK,SAAS,WAAW,YAAY;CAC3C,IAAI,CAAC,MAAM,CAAC,IAAI;CAEhB,OAAO,KAAK,KAAK,OAAO,EAAE,KAAK,MAAM;EAAE;EAAI;CAAG,CAAC,EAAE,CAAC;AACpD;;;;;;;AAQA,SAAgB,cAAc,MAA4B,SAAqB;CAC7E,IAAI,OAAO,WAAW,aAAa;CACnC,OAAO,SAAS,CAAC;CACjB,OAAO,KAAK,KAAK,WAAW,MAAM,OAAO;AAC3C;;;;;;;;;;;;;;;;;;;;;;AAuBA,IAAI;AAEJ,SAAgB,aAAa,SAA2C;CACtE,gBAAgB,QAAQ,eAAe,KAAA,IAAY,KAAA,IAAY,OAAO,QAAQ,UAAU;AAC1F;;;;;;;AAQA,eAAsB,eAAe,QAAgC;CACnE,IAAI,CAAC,eAAe;CACpB,cAAc;EAAE,YAAY;EAAe,WAAW,UAAU;EAAG;CAAO,CAAC;AAC7E;AAEA,SAAgB,cAAc,SAK3B;CACD,IAAI,OAAO,WAAW,eAAe,OAAO,UAAU,aAAa;CACnE,IAAI,QAAQ,GAAG;CAEf,MAAM,QAAQ,QAAiB;EAC7B,MAAM,SAAS,IAAI,gBAAgB;GACjC,MAAM,SAAS,QAAQ;GACvB,KAAK,QAAQ;EACf,CAAC;EACD,IAAI,KAAK,OAAO,IAAI,OAAO,GAAG;EAC9B,IAAI,MAAM,CAAC,CAAC,MAAM,4BAA4B,OAAO,SAAS;CAChE;CAEA,IAAI,CAAC,QAAQ,QAAQ;EACnB,KAAK;EACL;CACF;CACA,OAAO,QAAQ,MAAM,CAAC,CAAC,KAAK,YAAY,KAAK,CAAC;AAChD"}
|
package/dist/track/index.cjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_setup_index = require("../setup/index.cjs");
|
|
3
|
-
const require_constants_storage = require("../constants/storage.cjs");
|
|
4
3
|
const require_setup_session = require("../setup/session.cjs");
|
|
5
4
|
const require_third_parties_ignored_events = require("../third-parties/ignored-events.cjs");
|
|
6
5
|
const require_visitor_index = require("../visitor/index.cjs");
|
|
@@ -98,7 +97,7 @@ async function flush() {
|
|
|
98
97
|
let events = [];
|
|
99
98
|
try {
|
|
100
99
|
await getTokenBucket().removeTokens();
|
|
101
|
-
const visitor_id =
|
|
100
|
+
const visitor_id = require_visitor_index.visitorId();
|
|
102
101
|
const headers = await require_setup_index.config.getHeaders();
|
|
103
102
|
await Promise.all(list.map((event) => event.tags));
|
|
104
103
|
events = list.splice(0);
|
|
@@ -153,8 +152,8 @@ function track(name, properties, options = defaultOptions) {
|
|
|
153
152
|
}
|
|
154
153
|
/**
|
|
155
154
|
* Sends the event now, with whatever is queued, and resolves once it has been sent or lost — or,
|
|
156
|
-
* when
|
|
157
|
-
* send. It never rejects: tracking must not fail the step that awaits it.
|
|
155
|
+
* when the send could not start (its headers or rate limit threw), at once, the event left queued
|
|
156
|
+
* for the next send. It never rejects: tracking must not fail the step that awaits it.
|
|
158
157
|
*/
|
|
159
158
|
async function trackAsync(name, properties, options = defaultOptions) {
|
|
160
159
|
let settle;
|
|
@@ -176,17 +175,6 @@ async function trackAsync(name, properties, options = defaultOptions) {
|
|
|
176
175
|
if (list.includes(event)) return;
|
|
177
176
|
await settled;
|
|
178
177
|
}
|
|
179
|
-
/**
|
|
180
|
-
* The visitor id a beacon can carry. It is persisted, so a returning visitor already has one
|
|
181
|
-
* before `getVisitor` has finished its round trip for this page. Requiring the in-memory copy
|
|
182
|
-
* threw away exactly the events a beacon exists for: everything a visit accrues before its first
|
|
183
|
-
* batch comes back, which for a short visit is the whole of it. A visitor the server has never
|
|
184
|
-
* seen has none: the server would reject its events.
|
|
185
|
-
*/
|
|
186
|
-
function beaconVisitorId() {
|
|
187
|
-
const stored = require_setup_index.config.storage.getItem(require_constants_storage.keys.visitor_id);
|
|
188
|
-
return require_setup_index.cache.visitor?.id ?? (stored && stored !== "undefined" ? stored : void 0);
|
|
189
|
-
}
|
|
190
178
|
function beacon(dto) {
|
|
191
179
|
if (typeof navigator === "undefined" || !("sendBeacon" in navigator)) return false;
|
|
192
180
|
const blob = new Blob([JSON.stringify(dto)], { type: "text/plain;charset=UTF-8" });
|
|
@@ -199,24 +187,23 @@ function beacon(dto) {
|
|
|
199
187
|
*
|
|
200
188
|
* Nothing can be awaited here, so the events go with the tags they have settled on (the last
|
|
201
189
|
* built ones otherwise), and without the server's ids: their third-party trackers are not fired,
|
|
202
|
-
* as an id-less browser event could not be deduplicated against the server's.
|
|
203
|
-
*
|
|
204
|
-
*
|
|
190
|
+
* as an id-less browser event could not be deduplicated against the server's. The visitor id is
|
|
191
|
+
* local (`visitorId`), so a first visit left within a second is sent like any other; when no
|
|
192
|
+
* beacon can take them — no `sendBeacon`, a refused body — they stay queued for the usual send.
|
|
205
193
|
*/
|
|
206
194
|
function sendPendingEvents() {
|
|
207
195
|
if (list.length === 0) return;
|
|
208
|
-
const visitor_id =
|
|
209
|
-
|
|
196
|
+
const visitor_id = require_visitor_index.visitorId();
|
|
197
|
+
if (!beacon(list.map((event) => ({
|
|
210
198
|
name: event.name,
|
|
211
199
|
properties: event.properties,
|
|
212
200
|
tags: event.settled ?? require_setup_index.cache.tags ?? {},
|
|
213
|
-
visitor_id
|
|
201
|
+
visitor_id,
|
|
214
202
|
session_id: event.session_id,
|
|
215
203
|
platform: require_setup_index.config.platform,
|
|
216
204
|
environment: require_setup_index.config.environment,
|
|
217
205
|
timestamp: event.timestamp
|
|
218
|
-
}))
|
|
219
|
-
if (!visitor_id || !beacon(dto)) {
|
|
206
|
+
})))) {
|
|
220
207
|
flush();
|
|
221
208
|
return;
|
|
222
209
|
}
|
|
@@ -227,8 +214,7 @@ function sendPendingEvents() {
|
|
|
227
214
|
list.splice(0).forEach((event) => event.options.onSucceed?.(void 0));
|
|
228
215
|
}
|
|
229
216
|
function sendBeacon(name, properties) {
|
|
230
|
-
const visitor_id =
|
|
231
|
-
if (!visitor_id) return;
|
|
217
|
+
const visitor_id = require_visitor_index.visitorId();
|
|
232
218
|
const session_id = require_setup_session.getSession().extend();
|
|
233
219
|
if (!session_id) return;
|
|
234
220
|
if (beacon([{
|
package/dist/track/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["TokenBucket","config","cache","getSession","getVisitor","fetch","IGNORED_EVENTS","keys"],"sources":["../../src/track/index.ts"],"sourcesContent":["import { TokenBucket, fetch } from '@shware/utils';\nimport { keys } from '../constants/storage';\nimport type { CreateTrackEventDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { getVisitor } from '../visitor/index';\nimport type { EventName, TrackEventResponse, TrackName, TrackProperties, TrackTags } from './types';\n\nexport interface TrackOptions {\n enableThirdPartyTracking?: boolean;\n onSucceed?: (response?: TrackEventResponse[number]) => void;\n onError?: (error: unknown) => void;\n}\n\nconst defaultOptions: TrackOptions = {};\n\nlet tokenBucket: TokenBucket | undefined;\n\n/**\n * The rate limiter, built on the first send rather than at module scope: its\n * constructor starts a refill `setInterval`, and Cloudflare Workers refuse to\n * set a timer outside a request handler for the same reason they refuse to\n * generate random values there — see `setup/session.ts`.\n */\nfunction getTokenBucket() {\n return (tokenBucket ??= new TokenBucket({ rate: 1, capacity: 20, requested: 2 }));\n}\n\ntype Item = {\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n name: TrackName<any>;\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n properties: TrackProperties<any>;\n tags: Promise<TrackTags>;\n /** `tags` once settled: a page being hidden can await nothing, and sends what it has. */\n settled?: TrackTags;\n /** Decided when the event happens, as GA4 and PostHog decide it, never when it is sent. */\n session_id: string;\n timestamp: string;\n options: TrackOptions;\n};\n\n/**\n * Tags belong to the moment the event happened, not to the moment its batch goes out: a queued\n * event waits up to `delay` ms, and a single page app can navigate in that window, which would\n * stamp every pending event with the URL of the page the user has already left.\n *\n * The promise then sits in the queue with nothing awaiting it, so a failure has to be absorbed\n * here — an unhandled rejection would surface as a global error long before `sendEvents` could\n * catch it. Falling back to the last built tags keeps the event, minus whatever changed since.\n */\nasync function captureTags(): Promise<TrackTags> {\n try {\n return await config.getTags();\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n return cache.tags ?? {};\n }\n}\n\n/**\n * The events not yet sent, and the only place they are: a batch leaves it at the moment it is\n * handed to `fetch` or to a beacon, never earlier. So when the page is hidden or left,\n * everything still unsent is right here for `sendPendingEvents`.\n */\nconst list: Item[] = [];\nconst batchSize = 10;\nconst delay = 2000;\nlet timer: ReturnType<typeof setTimeout> | null = null;\n\n/**\n * Queues an event, preceded by a `session_start` when it is the one that opens a session.\n *\n * The session is decided here, when the event happens, as GA4 and PostHog decide it: the\n * `session_start` then sits in the queue right before the event that opened the session, so\n * wherever that event goes — a batch, a beacon, a retry — its announcement has gone ahead of it.\n * It carries that event's tags and time, as GA4 derives it from the hit that started the\n * session: the landing page is where the session began, whatever the page has navigated to by\n * the time the batch goes out.\n */\nfunction enqueue(name: Item['name'], properties: Item['properties'], options: TrackOptions): Item {\n const timestamp = new Date().toISOString();\n const tags = captureTags();\n const { id: session_id, started } = getSession().touch(Date.parse(timestamp));\n const item = (fields: Pick<Item, 'name' | 'properties' | 'options'>): Item => {\n const result: Item = { ...fields, tags, timestamp, session_id };\n void tags.then((settled) => (result.settled = settled));\n return result;\n };\n if (started) {\n const options = { enableThirdPartyTracking: false };\n list.push(item({ name: 'session_start', properties: {}, options }));\n }\n const event = item({ name, properties, options });\n list.push(event);\n return event;\n}\n\n/**\n * Sends the queue. Everything that has to be awaited is awaited first, and only then are the\n * events taken off the queue and handed to `fetch` in the same synchronous step — so there is\n * no moment at which an event is neither queued nor sent.\n */\nasync function flush() {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n if (list.length === 0) return;\n\n let events: Item[] = [];\n try {\n await getTokenBucket().removeTokens();\n const visitor_id = (await getVisitor()).id;\n const headers = await config.getHeaders();\n // Settled before the events leave the queue; those queued meanwhile are a microtask away.\n await Promise.all(list.map((event) => event.tags));\n\n events = list.splice(0);\n if (events.length === 0) return;\n const dto: CreateTrackEventDTO = await Promise.all(\n events.map(async (event) => ({\n name: event.name,\n properties: event.properties,\n tags: await event.tags,\n visitor_id,\n session_id: event.session_id,\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }))\n );\n\n const body = JSON.stringify(dto);\n const response = await fetch(`${config.endpoint}/events`, {\n method: 'POST',\n credentials: 'include',\n // keepalive lets the request survive the page being unloaded mid-flight, within a 64KB\n // budget shared by every such request of the page; a body that could not fit goes as a\n // plain POST rather than failing outright.\n keepalive: body.length < 60_000,\n headers,\n body,\n });\n\n if (!response.ok) {\n throw new Error(`Failed to send track event: ${response.status} ${await response.text()}`);\n }\n\n const data = (await response.json()) as TrackEventResponse;\n\n events.forEach((event, index) => {\n const eventId = data.at(index)?.id;\n event.options.onSucceed?.(eventId ? { id: eventId } : undefined);\n // An explicit false, not falsiness: a caller passing `{ onSucceed }` replaces the options\n // object wholesale, and leaving the flag out must not silently switch forwarding off.\n if (event.options.enableThirdPartyTracking === false || IGNORED_EVENTS.includes(event.name)) {\n return;\n }\n config.thirdPartyTrackers.forEach((tracker) => {\n try {\n tracker(event.name, event.properties, eventId);\n } catch (e: unknown) {\n // A third-party script does not get to take the rest of the batch with it.\n if (e instanceof Error) console.log(e.message);\n }\n });\n });\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n // `fetch` has already retried the transient failures by the time a batch fails here; what\n // remains is a batch the server rejected outright — one invalid event fails the whole batch\n // — or a network that stayed down past the retries. The other events are reported lost;\n // `session_start` alone goes back on the queue for the next send, because it is the\n // session's only attribution record: a session without one has no channel, and its every\n // later event drops out of an attribution join.\n const starts = events.filter((event) => event.name === 'session_start');\n list.unshift(...starts);\n events\n .filter((event) => event.name !== 'session_start')\n .forEach((event) => event.options.onError?.(e));\n }\n}\n\nexport function track<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n enqueue(name, properties, options);\n if (list.length >= batchSize) {\n void flush();\n return;\n }\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => void flush(), delay);\n}\n\n/**\n * Sends the event now, with whatever is queued, and resolves once it has been sent or lost — or,\n * when no visitor could be created to send it with, at once, the event left queued for the next\n * send. It never rejects: tracking must not fail the step that awaits it.\n */\nexport async function trackAsync<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n let settle!: () => void;\n const settled = new Promise<void>((resolve) => {\n settle = resolve;\n });\n const event = enqueue(name, properties, {\n ...options,\n onSucceed: (response) => {\n options.onSucceed?.(response);\n settle();\n },\n onError: (error) => {\n options.onError?.(error);\n settle();\n },\n });\n await flush();\n // Still queued: this send could not take it — no visitor id to send it with yet. It waits for\n // the next send, but the caller, who may be holding a navigation on this, does not.\n if (list.includes(event)) return;\n await settled;\n}\n\n/**\n * The visitor id a beacon can carry. It is persisted, so a returning visitor already has one\n * before `getVisitor` has finished its round trip for this page. Requiring the in-memory copy\n * threw away exactly the events a beacon exists for: everything a visit accrues before its first\n * batch comes back, which for a short visit is the whole of it. A visitor the server has never\n * seen has none: the server would reject its events.\n */\nfunction beaconVisitorId(): string | undefined {\n const stored = config.storage.getItem(keys.visitor_id);\n return cache.visitor?.id ?? (stored && stored !== 'undefined' ? stored : undefined);\n}\n\nfunction beacon(dto: CreateTrackEventDTO): boolean {\n // Not every runtime with a `navigator` has it: React Native has neither beacons nor pages.\n if (typeof navigator === 'undefined' || !('sendBeacon' in navigator)) return false;\n // text/plain keeps a cross-origin beacon a CORS simple request: as application/json it needs a\n // preflight, which a page being closed often cannot complete, and the beacon is then dropped.\n // The server reads it as JSON (`zBeaconJson` in @shware/http).\n const blob = new Blob([JSON.stringify(dto)], { type: 'text/plain;charset=UTF-8' });\n return navigator.sendBeacon(`${config.endpoint}/events`, blob);\n}\n\n/**\n * Sends the queue by beacon, now: for when the page is hidden or left, the last moment its\n * script is sure to run — what PostHog's `requestQueue.unload()` and GA4 do on the same events.\n * Call it before the engagement beacon: a new session's `session_start` is in this queue.\n *\n * Nothing can be awaited here, so the events go with the tags they have settled on (the last\n * built ones otherwise), and without the server's ids: their third-party trackers are not fired,\n * as an id-less browser event could not be deduplicated against the server's. When no beacon can\n * take them — a visitor the server does not know yet, no `sendBeacon`, a refused body — they stay\n * queued for the usual send, which also creates the visitor.\n */\nexport function sendPendingEvents() {\n if (list.length === 0) return;\n const visitor_id = beaconVisitorId();\n const dto: CreateTrackEventDTO = list.map((event) => ({\n name: event.name,\n properties: event.properties,\n tags: event.settled ?? cache.tags ?? {},\n visitor_id: visitor_id ?? '',\n session_id: event.session_id,\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }));\n if (!visitor_id || !beacon(dto)) {\n void flush();\n return;\n }\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n list.splice(0).forEach((event) => event.options.onSucceed?.(undefined));\n}\n\nexport function sendBeacon<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n) {\n const visitor_id = beaconVisitorId();\n if (!visitor_id) return;\n // No stored session means no event was ever queued from this storage, so there is no session\n // to report for — and one started here could never be announced. See `Session.extend`.\n const session_id = getSession().extend();\n if (!session_id) return;\n\n const dto: CreateTrackEventDTO = [\n {\n name,\n properties,\n // Tags are worth less than the event carrying them: an empty set still reports the\n // engagement, and every field in `tagsSchema` is optional.\n tags: cache.tags ?? {},\n visitor_id,\n session_id,\n platform: config.platform,\n environment: config.environment,\n timestamp: new Date().toISOString(),\n },\n ];\n if (beacon(dto)) return;\n console.warn('Failed to send beacon', name, properties);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["TokenBucket","config","cache","getSession","visitorId","fetch","IGNORED_EVENTS"],"sources":["../../src/track/index.ts"],"sourcesContent":["import { TokenBucket, fetch } from '@shware/utils';\nimport type { CreateTrackEventDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { visitorId } from '../visitor/index';\nimport type { EventName, TrackEventResponse, TrackName, TrackProperties, TrackTags } from './types';\n\nexport interface TrackOptions {\n enableThirdPartyTracking?: boolean;\n onSucceed?: (response?: TrackEventResponse[number]) => void;\n onError?: (error: unknown) => void;\n}\n\nconst defaultOptions: TrackOptions = {};\n\nlet tokenBucket: TokenBucket | undefined;\n\n/**\n * The rate limiter, built on the first send rather than at module scope: its\n * constructor starts a refill `setInterval`, and Cloudflare Workers refuse to\n * set a timer outside a request handler for the same reason they refuse to\n * generate random values there — see `setup/session.ts`.\n */\nfunction getTokenBucket() {\n return (tokenBucket ??= new TokenBucket({ rate: 1, capacity: 20, requested: 2 }));\n}\n\ntype Item = {\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n name: TrackName<any>;\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n properties: TrackProperties<any>;\n tags: Promise<TrackTags>;\n /** `tags` once settled: a page being hidden can await nothing, and sends what it has. */\n settled?: TrackTags;\n /** Decided when the event happens, as GA4 and PostHog decide it, never when it is sent. */\n session_id: string;\n timestamp: string;\n options: TrackOptions;\n};\n\n/**\n * Tags belong to the moment the event happened, not to the moment its batch goes out: a queued\n * event waits up to `delay` ms, and a single page app can navigate in that window, which would\n * stamp every pending event with the URL of the page the user has already left.\n *\n * The promise then sits in the queue with nothing awaiting it, so a failure has to be absorbed\n * here — an unhandled rejection would surface as a global error long before `sendEvents` could\n * catch it. Falling back to the last built tags keeps the event, minus whatever changed since.\n */\nasync function captureTags(): Promise<TrackTags> {\n try {\n return await config.getTags();\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n return cache.tags ?? {};\n }\n}\n\n/**\n * The events not yet sent, and the only place they are: a batch leaves it at the moment it is\n * handed to `fetch` or to a beacon, never earlier. So when the page is hidden or left,\n * everything still unsent is right here for `sendPendingEvents`.\n */\nconst list: Item[] = [];\nconst batchSize = 10;\nconst delay = 2000;\nlet timer: ReturnType<typeof setTimeout> | null = null;\n\n/**\n * Queues an event, preceded by a `session_start` when it is the one that opens a session.\n *\n * The session is decided here, when the event happens, as GA4 and PostHog decide it: the\n * `session_start` then sits in the queue right before the event that opened the session, so\n * wherever that event goes — a batch, a beacon, a retry — its announcement has gone ahead of it.\n * It carries that event's tags and time, as GA4 derives it from the hit that started the\n * session: the landing page is where the session began, whatever the page has navigated to by\n * the time the batch goes out.\n */\nfunction enqueue(name: Item['name'], properties: Item['properties'], options: TrackOptions): Item {\n const timestamp = new Date().toISOString();\n const tags = captureTags();\n const { id: session_id, started } = getSession().touch(Date.parse(timestamp));\n const item = (fields: Pick<Item, 'name' | 'properties' | 'options'>): Item => {\n const result: Item = { ...fields, tags, timestamp, session_id };\n void tags.then((settled) => (result.settled = settled));\n return result;\n };\n if (started) {\n const options = { enableThirdPartyTracking: false };\n list.push(item({ name: 'session_start', properties: {}, options }));\n }\n const event = item({ name, properties, options });\n list.push(event);\n return event;\n}\n\n/**\n * Sends the queue. Everything that has to be awaited is awaited first, and only then are the\n * events taken off the queue and handed to `fetch` in the same synchronous step — so there is\n * no moment at which an event is neither queued nor sent.\n */\nasync function flush() {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n if (list.length === 0) return;\n\n let events: Item[] = [];\n try {\n await getTokenBucket().removeTokens();\n const visitor_id = visitorId();\n const headers = await config.getHeaders();\n // Settled before the events leave the queue; those queued meanwhile are a microtask away.\n await Promise.all(list.map((event) => event.tags));\n\n events = list.splice(0);\n if (events.length === 0) return;\n const dto: CreateTrackEventDTO = await Promise.all(\n events.map(async (event) => ({\n name: event.name,\n properties: event.properties,\n tags: await event.tags,\n visitor_id,\n session_id: event.session_id,\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }))\n );\n\n const body = JSON.stringify(dto);\n const response = await fetch(`${config.endpoint}/events`, {\n method: 'POST',\n credentials: 'include',\n // keepalive lets the request survive the page being unloaded mid-flight, within a 64KB\n // budget shared by every such request of the page; a body that could not fit goes as a\n // plain POST rather than failing outright.\n keepalive: body.length < 60_000,\n headers,\n body,\n });\n\n if (!response.ok) {\n throw new Error(`Failed to send track event: ${response.status} ${await response.text()}`);\n }\n\n const data = (await response.json()) as TrackEventResponse;\n\n events.forEach((event, index) => {\n const eventId = data.at(index)?.id;\n event.options.onSucceed?.(eventId ? { id: eventId } : undefined);\n // An explicit false, not falsiness: a caller passing `{ onSucceed }` replaces the options\n // object wholesale, and leaving the flag out must not silently switch forwarding off.\n if (event.options.enableThirdPartyTracking === false || IGNORED_EVENTS.includes(event.name)) {\n return;\n }\n config.thirdPartyTrackers.forEach((tracker) => {\n try {\n tracker(event.name, event.properties, eventId);\n } catch (e: unknown) {\n // A third-party script does not get to take the rest of the batch with it.\n if (e instanceof Error) console.log(e.message);\n }\n });\n });\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n // `fetch` has already retried the transient failures by the time a batch fails here; what\n // remains is a batch the server rejected outright — one invalid event fails the whole batch\n // — or a network that stayed down past the retries. The other events are reported lost;\n // `session_start` alone goes back on the queue for the next send, because it is the\n // session's only attribution record: a session without one has no channel, and its every\n // later event drops out of an attribution join.\n const starts = events.filter((event) => event.name === 'session_start');\n list.unshift(...starts);\n events\n .filter((event) => event.name !== 'session_start')\n .forEach((event) => event.options.onError?.(e));\n }\n}\n\nexport function track<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n enqueue(name, properties, options);\n if (list.length >= batchSize) {\n void flush();\n return;\n }\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => void flush(), delay);\n}\n\n/**\n * Sends the event now, with whatever is queued, and resolves once it has been sent or lost — or,\n * when the send could not start (its headers or rate limit threw), at once, the event left queued\n * for the next send. It never rejects: tracking must not fail the step that awaits it.\n */\nexport async function trackAsync<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n let settle!: () => void;\n const settled = new Promise<void>((resolve) => {\n settle = resolve;\n });\n const event = enqueue(name, properties, {\n ...options,\n onSucceed: (response) => {\n options.onSucceed?.(response);\n settle();\n },\n onError: (error) => {\n options.onError?.(error);\n settle();\n },\n });\n await flush();\n // Still queued: this send failed before taking it. It waits for the next send, but the caller,\n // who may be holding a navigation on this, does not.\n if (list.includes(event)) return;\n await settled;\n}\n\nfunction beacon(dto: CreateTrackEventDTO): boolean {\n // Not every runtime with a `navigator` has it: React Native has neither beacons nor pages.\n if (typeof navigator === 'undefined' || !('sendBeacon' in navigator)) return false;\n // text/plain keeps a cross-origin beacon a CORS simple request: as application/json it needs a\n // preflight, which a page being closed often cannot complete, and the beacon is then dropped.\n // The server reads it as JSON (`zBeaconJson` in @shware/http).\n const blob = new Blob([JSON.stringify(dto)], { type: 'text/plain;charset=UTF-8' });\n return navigator.sendBeacon(`${config.endpoint}/events`, blob);\n}\n\n/**\n * Sends the queue by beacon, now: for when the page is hidden or left, the last moment its\n * script is sure to run — what PostHog's `requestQueue.unload()` and GA4 do on the same events.\n * Call it before the engagement beacon: a new session's `session_start` is in this queue.\n *\n * Nothing can be awaited here, so the events go with the tags they have settled on (the last\n * built ones otherwise), and without the server's ids: their third-party trackers are not fired,\n * as an id-less browser event could not be deduplicated against the server's. The visitor id is\n * local (`visitorId`), so a first visit left within a second is sent like any other; when no\n * beacon can take them — no `sendBeacon`, a refused body — they stay queued for the usual send.\n */\nexport function sendPendingEvents() {\n if (list.length === 0) return;\n const visitor_id = visitorId();\n const dto: CreateTrackEventDTO = list.map((event) => ({\n name: event.name,\n properties: event.properties,\n tags: event.settled ?? cache.tags ?? {},\n visitor_id,\n session_id: event.session_id,\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }));\n if (!beacon(dto)) {\n void flush();\n return;\n }\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n list.splice(0).forEach((event) => event.options.onSucceed?.(undefined));\n}\n\nexport function sendBeacon<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n) {\n const visitor_id = visitorId();\n // No stored session means no event was ever queued from this storage, so there is no session\n // to report for — and one started here could never be announced. See `Session.extend`.\n const session_id = getSession().extend();\n if (!session_id) return;\n\n const dto: CreateTrackEventDTO = [\n {\n name,\n properties,\n // Tags are worth less than the event carrying them: an empty set still reports the\n // engagement, and every field in `tagsSchema` is optional.\n tags: cache.tags ?? {},\n visitor_id,\n session_id,\n platform: config.platform,\n environment: config.environment,\n timestamp: new Date().toISOString(),\n },\n ];\n if (beacon(dto)) return;\n console.warn('Failed to send beacon', name, properties);\n}\n"],"mappings":"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"}
|
package/dist/track/index.d.cts
CHANGED
|
@@ -8,8 +8,8 @@ export interface TrackOptions {
|
|
|
8
8
|
export declare function track<T extends EventName = EventName>(name: TrackName<T>, properties?: TrackProperties<T>, options?: TrackOptions): void;
|
|
9
9
|
/**
|
|
10
10
|
* Sends the event now, with whatever is queued, and resolves once it has been sent or lost — or,
|
|
11
|
-
* when
|
|
12
|
-
* send. It never rejects: tracking must not fail the step that awaits it.
|
|
11
|
+
* when the send could not start (its headers or rate limit threw), at once, the event left queued
|
|
12
|
+
* for the next send. It never rejects: tracking must not fail the step that awaits it.
|
|
13
13
|
*/
|
|
14
14
|
export declare function trackAsync<T extends EventName = EventName>(name: TrackName<T>, properties?: TrackProperties<T>, options?: TrackOptions): Promise<void>;
|
|
15
15
|
/**
|
|
@@ -19,9 +19,9 @@ export declare function trackAsync<T extends EventName = EventName>(name: TrackN
|
|
|
19
19
|
*
|
|
20
20
|
* Nothing can be awaited here, so the events go with the tags they have settled on (the last
|
|
21
21
|
* built ones otherwise), and without the server's ids: their third-party trackers are not fired,
|
|
22
|
-
* as an id-less browser event could not be deduplicated against the server's.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* as an id-less browser event could not be deduplicated against the server's. The visitor id is
|
|
23
|
+
* local (`visitorId`), so a first visit left within a second is sent like any other; when no
|
|
24
|
+
* beacon can take them — no `sendBeacon`, a refused body — they stay queued for the usual send.
|
|
25
25
|
*/
|
|
26
26
|
export declare function sendPendingEvents(): void;
|
|
27
27
|
export declare function sendBeacon<T extends EventName = EventName>(name: TrackName<T>, properties?: TrackProperties<T>): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/track/index.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/track/index.ts"],"mappings":";;iBAQiB;EACf;EACA,aAAa,WAAW;EACxB,WAAW;;wBA6KG,MAAM,UAAU,YAAY,WAC1C,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS;;;;;;wBAgBW,WAAW,UAAU,YAAY,WACrD,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS,eAA6B;;;;;;;;;;;;wBA6CxB;wBAwBA,WAAW,UAAU,YAAY,WAC/C,MAAM,UAAU,IAChB,aAAa,gBAAgB"}
|
package/dist/track/index.d.mts
CHANGED
|
@@ -8,8 +8,8 @@ export interface TrackOptions {
|
|
|
8
8
|
export declare function track<T extends EventName = EventName>(name: TrackName<T>, properties?: TrackProperties<T>, options?: TrackOptions): void;
|
|
9
9
|
/**
|
|
10
10
|
* Sends the event now, with whatever is queued, and resolves once it has been sent or lost — or,
|
|
11
|
-
* when
|
|
12
|
-
* send. It never rejects: tracking must not fail the step that awaits it.
|
|
11
|
+
* when the send could not start (its headers or rate limit threw), at once, the event left queued
|
|
12
|
+
* for the next send. It never rejects: tracking must not fail the step that awaits it.
|
|
13
13
|
*/
|
|
14
14
|
export declare function trackAsync<T extends EventName = EventName>(name: TrackName<T>, properties?: TrackProperties<T>, options?: TrackOptions): Promise<void>;
|
|
15
15
|
/**
|
|
@@ -19,9 +19,9 @@ export declare function trackAsync<T extends EventName = EventName>(name: TrackN
|
|
|
19
19
|
*
|
|
20
20
|
* Nothing can be awaited here, so the events go with the tags they have settled on (the last
|
|
21
21
|
* built ones otherwise), and without the server's ids: their third-party trackers are not fired,
|
|
22
|
-
* as an id-less browser event could not be deduplicated against the server's.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* as an id-less browser event could not be deduplicated against the server's. The visitor id is
|
|
23
|
+
* local (`visitorId`), so a first visit left within a second is sent like any other; when no
|
|
24
|
+
* beacon can take them — no `sendBeacon`, a refused body — they stay queued for the usual send.
|
|
25
25
|
*/
|
|
26
26
|
export declare function sendPendingEvents(): void;
|
|
27
27
|
export declare function sendBeacon<T extends EventName = EventName>(name: TrackName<T>, properties?: TrackProperties<T>): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/track/index.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/track/index.ts"],"mappings":";;iBAQiB;EACf;EACA,aAAa,WAAW;EACxB,WAAW;;wBA6KG,MAAM,UAAU,YAAY,WAC1C,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS;;;;;;wBAgBW,WAAW,UAAU,YAAY,WACrD,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS,eAA6B;;;;;;;;;;;;wBA6CxB;wBAwBA,WAAW,UAAU,YAAY,WAC/C,MAAM,UAAU,IAChB,aAAa,gBAAgB"}
|
package/dist/track/index.mjs
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { cache, config } from "../setup/index.mjs";
|
|
2
|
-
import { keys } from "../constants/storage.mjs";
|
|
3
2
|
import { getSession } from "../setup/session.mjs";
|
|
4
3
|
import { IGNORED_EVENTS } from "../third-parties/ignored-events.mjs";
|
|
5
|
-
import {
|
|
4
|
+
import { visitorId } from "../visitor/index.mjs";
|
|
6
5
|
import { TokenBucket, fetch } from "@shware/utils";
|
|
7
6
|
//#region src/track/index.ts
|
|
8
7
|
const defaultOptions = {};
|
|
@@ -97,7 +96,7 @@ async function flush() {
|
|
|
97
96
|
let events = [];
|
|
98
97
|
try {
|
|
99
98
|
await getTokenBucket().removeTokens();
|
|
100
|
-
const visitor_id = (
|
|
99
|
+
const visitor_id = visitorId();
|
|
101
100
|
const headers = await config.getHeaders();
|
|
102
101
|
await Promise.all(list.map((event) => event.tags));
|
|
103
102
|
events = list.splice(0);
|
|
@@ -152,8 +151,8 @@ function track(name, properties, options = defaultOptions) {
|
|
|
152
151
|
}
|
|
153
152
|
/**
|
|
154
153
|
* Sends the event now, with whatever is queued, and resolves once it has been sent or lost — or,
|
|
155
|
-
* when
|
|
156
|
-
* send. It never rejects: tracking must not fail the step that awaits it.
|
|
154
|
+
* when the send could not start (its headers or rate limit threw), at once, the event left queued
|
|
155
|
+
* for the next send. It never rejects: tracking must not fail the step that awaits it.
|
|
157
156
|
*/
|
|
158
157
|
async function trackAsync(name, properties, options = defaultOptions) {
|
|
159
158
|
let settle;
|
|
@@ -175,17 +174,6 @@ async function trackAsync(name, properties, options = defaultOptions) {
|
|
|
175
174
|
if (list.includes(event)) return;
|
|
176
175
|
await settled;
|
|
177
176
|
}
|
|
178
|
-
/**
|
|
179
|
-
* The visitor id a beacon can carry. It is persisted, so a returning visitor already has one
|
|
180
|
-
* before `getVisitor` has finished its round trip for this page. Requiring the in-memory copy
|
|
181
|
-
* threw away exactly the events a beacon exists for: everything a visit accrues before its first
|
|
182
|
-
* batch comes back, which for a short visit is the whole of it. A visitor the server has never
|
|
183
|
-
* seen has none: the server would reject its events.
|
|
184
|
-
*/
|
|
185
|
-
function beaconVisitorId() {
|
|
186
|
-
const stored = config.storage.getItem(keys.visitor_id);
|
|
187
|
-
return cache.visitor?.id ?? (stored && stored !== "undefined" ? stored : void 0);
|
|
188
|
-
}
|
|
189
177
|
function beacon(dto) {
|
|
190
178
|
if (typeof navigator === "undefined" || !("sendBeacon" in navigator)) return false;
|
|
191
179
|
const blob = new Blob([JSON.stringify(dto)], { type: "text/plain;charset=UTF-8" });
|
|
@@ -198,24 +186,23 @@ function beacon(dto) {
|
|
|
198
186
|
*
|
|
199
187
|
* Nothing can be awaited here, so the events go with the tags they have settled on (the last
|
|
200
188
|
* built ones otherwise), and without the server's ids: their third-party trackers are not fired,
|
|
201
|
-
* as an id-less browser event could not be deduplicated against the server's.
|
|
202
|
-
*
|
|
203
|
-
*
|
|
189
|
+
* as an id-less browser event could not be deduplicated against the server's. The visitor id is
|
|
190
|
+
* local (`visitorId`), so a first visit left within a second is sent like any other; when no
|
|
191
|
+
* beacon can take them — no `sendBeacon`, a refused body — they stay queued for the usual send.
|
|
204
192
|
*/
|
|
205
193
|
function sendPendingEvents() {
|
|
206
194
|
if (list.length === 0) return;
|
|
207
|
-
const visitor_id =
|
|
208
|
-
|
|
195
|
+
const visitor_id = visitorId();
|
|
196
|
+
if (!beacon(list.map((event) => ({
|
|
209
197
|
name: event.name,
|
|
210
198
|
properties: event.properties,
|
|
211
199
|
tags: event.settled ?? cache.tags ?? {},
|
|
212
|
-
visitor_id
|
|
200
|
+
visitor_id,
|
|
213
201
|
session_id: event.session_id,
|
|
214
202
|
platform: config.platform,
|
|
215
203
|
environment: config.environment,
|
|
216
204
|
timestamp: event.timestamp
|
|
217
|
-
}))
|
|
218
|
-
if (!visitor_id || !beacon(dto)) {
|
|
205
|
+
})))) {
|
|
219
206
|
flush();
|
|
220
207
|
return;
|
|
221
208
|
}
|
|
@@ -226,8 +213,7 @@ function sendPendingEvents() {
|
|
|
226
213
|
list.splice(0).forEach((event) => event.options.onSucceed?.(void 0));
|
|
227
214
|
}
|
|
228
215
|
function sendBeacon(name, properties) {
|
|
229
|
-
const visitor_id =
|
|
230
|
-
if (!visitor_id) return;
|
|
216
|
+
const visitor_id = visitorId();
|
|
231
217
|
const session_id = getSession().extend();
|
|
232
218
|
if (!session_id) return;
|
|
233
219
|
if (beacon([{
|