@shware/analytics 9.0.0 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/hooks/use-web-analytics.cjs +5 -1
  2. package/dist/hooks/use-web-analytics.cjs.map +1 -1
  3. package/dist/hooks/use-web-analytics.d.cts.map +1 -1
  4. package/dist/hooks/use-web-analytics.d.mts.map +1 -1
  5. package/dist/hooks/use-web-analytics.mjs +6 -2
  6. package/dist/hooks/use-web-analytics.mjs.map +1 -1
  7. package/dist/index.cjs +1 -0
  8. package/dist/index.d.cts +2 -2
  9. package/dist/index.d.mts +2 -2
  10. package/dist/index.mjs +2 -2
  11. package/dist/server/google-data-manager.cjs +3 -3
  12. package/dist/server/google-data-manager.cjs.map +1 -1
  13. package/dist/server/google-data-manager.d.cts +15 -2
  14. package/dist/server/google-data-manager.d.cts.map +1 -1
  15. package/dist/server/google-data-manager.d.mts +15 -2
  16. package/dist/server/google-data-manager.d.mts.map +1 -1
  17. package/dist/server/google-data-manager.mjs +3 -3
  18. package/dist/server/google-data-manager.mjs.map +1 -1
  19. package/dist/server/index.d.cts +2 -2
  20. package/dist/server/index.d.mts +2 -2
  21. package/dist/server/linkedin-conversions-api.cjs +6 -1
  22. package/dist/server/linkedin-conversions-api.cjs.map +1 -1
  23. package/dist/server/linkedin-conversions-api.d.cts.map +1 -1
  24. package/dist/server/linkedin-conversions-api.d.mts.map +1 -1
  25. package/dist/server/linkedin-conversions-api.mjs +6 -1
  26. package/dist/server/linkedin-conversions-api.mjs.map +1 -1
  27. package/dist/server/meta-capi.cjs +7 -1
  28. package/dist/server/meta-capi.cjs.map +1 -1
  29. package/dist/server/meta-capi.d.cts.map +1 -1
  30. package/dist/server/meta-capi.d.mts.map +1 -1
  31. package/dist/server/meta-capi.mjs +7 -1
  32. package/dist/server/meta-capi.mjs.map +1 -1
  33. package/dist/server/meta-conversions-api.cjs +3 -1
  34. package/dist/server/meta-conversions-api.cjs.map +1 -1
  35. package/dist/server/meta-conversions-api.d.cts.map +1 -1
  36. package/dist/server/meta-conversions-api.d.mts.map +1 -1
  37. package/dist/server/meta-conversions-api.mjs +3 -1
  38. package/dist/server/meta-conversions-api.mjs.map +1 -1
  39. package/dist/server/microsoft-conversions-api.cjs +3 -1
  40. package/dist/server/microsoft-conversions-api.cjs.map +1 -1
  41. package/dist/server/microsoft-conversions-api.d.cts.map +1 -1
  42. package/dist/server/microsoft-conversions-api.d.mts.map +1 -1
  43. package/dist/server/microsoft-conversions-api.mjs +3 -1
  44. package/dist/server/microsoft-conversions-api.mjs.map +1 -1
  45. package/dist/server/openai-conversions-api.cjs +13 -12
  46. package/dist/server/openai-conversions-api.cjs.map +1 -1
  47. package/dist/server/openai-conversions-api.d.cts.map +1 -1
  48. package/dist/server/openai-conversions-api.d.mts.map +1 -1
  49. package/dist/server/openai-conversions-api.mjs +14 -13
  50. package/dist/server/openai-conversions-api.mjs.map +1 -1
  51. package/dist/server/reddit-conversions-api.cjs +6 -2
  52. package/dist/server/reddit-conversions-api.cjs.map +1 -1
  53. package/dist/server/reddit-conversions-api.d.cts +10 -1
  54. package/dist/server/reddit-conversions-api.d.cts.map +1 -1
  55. package/dist/server/reddit-conversions-api.d.mts +10 -1
  56. package/dist/server/reddit-conversions-api.d.mts.map +1 -1
  57. package/dist/server/reddit-conversions-api.mjs +6 -2
  58. package/dist/server/reddit-conversions-api.mjs.map +1 -1
  59. package/dist/setup/session.cjs +1 -1
  60. package/dist/setup/session.cjs.map +1 -1
  61. package/dist/setup/session.d.cts +6 -5
  62. package/dist/setup/session.d.cts.map +1 -1
  63. package/dist/setup/session.d.mts +6 -5
  64. package/dist/setup/session.d.mts.map +1 -1
  65. package/dist/setup/session.mjs +1 -1
  66. package/dist/setup/session.mjs.map +1 -1
  67. package/dist/third-parties/openai-pixel.cjs +29 -13
  68. package/dist/third-parties/openai-pixel.cjs.map +1 -1
  69. package/dist/third-parties/openai-pixel.d.cts +4 -3
  70. package/dist/third-parties/openai-pixel.d.cts.map +1 -1
  71. package/dist/third-parties/openai-pixel.d.mts +4 -3
  72. package/dist/third-parties/openai-pixel.d.mts.map +1 -1
  73. package/dist/third-parties/openai-pixel.mjs +30 -14
  74. package/dist/third-parties/openai-pixel.mjs.map +1 -1
  75. package/dist/track/index.cjs +156 -89
  76. package/dist/track/index.cjs.map +1 -1
  77. package/dist/track/index.d.cts +17 -0
  78. package/dist/track/index.d.cts.map +1 -1
  79. package/dist/track/index.d.mts +17 -0
  80. package/dist/track/index.d.mts.map +1 -1
  81. package/dist/track/index.mjs +156 -90
  82. package/dist/track/index.mjs.map +1 -1
  83. package/dist/track/oaiq.cjs +57 -9
  84. package/dist/track/oaiq.cjs.map +1 -1
  85. package/dist/track/oaiq.d.cts +33 -7
  86. package/dist/track/oaiq.d.cts.map +1 -1
  87. package/dist/track/oaiq.d.mts +33 -7
  88. package/dist/track/oaiq.d.mts.map +1 -1
  89. package/dist/track/oaiq.mjs +55 -10
  90. package/dist/track/oaiq.mjs.map +1 -1
  91. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"openai-pixel.mjs","names":[],"sources":["../../src/third-parties/openai-pixel.ts"],"sourcesContent":["import type { UpdateVisitorDTO } from '../schema/index';\nimport { NON_AD_EVENTS, type OAIQ, type OAIQUser, mapOAIEvent } from '../track/oaiq';\nimport type { EventName, TrackName, TrackProperties } from '../track/types';\nimport { getFirst } from '../utils/field';\nimport { sha256 } from '../utils/sha256';\n\ndeclare global {\n interface Window {\n /** Undefined until the OpenAI pixel script has loaded. */\n oaiq?: OAIQ['oaiq'];\n }\n}\n\n/** Drop `undefined` fields so the SDK only receives populated values. */\n// oxlint-disable-next-line @typescript-eslint/no-explicit-any\nfunction clean(value: unknown): any {\n return JSON.parse(JSON.stringify(value));\n}\n\n/**\n * Forward an internal track event to the OpenAI measurement pixel.\n * `eventId` is reused as the OpenAI `event_id` so browser events deduplicate against the\n * Conversions API. https://developers.openai.com/ads/measurement-pixel\n */\nexport function sendOpenAIEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n eventId?: string\n) {\n if (typeof window === 'undefined' || !window.oaiq) {\n console.warn('oaiq has not been initialized');\n return;\n }\n if (NON_AD_EVENTS.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 { type, data } = mapOAIEvent(name, properties);\n if (type === 'custom') {\n window.oaiq('measure', 'custom', clean(data), { event_id: eventId, custom_event_name: name });\n } else {\n window.oaiq('measure', type, clean(data), { event_id: eventId });\n }\n}\n\n/**\n * Re-initialize the pixel with hashed user identity for better conversion matching. Email and\n * external id are hashed client-side; geographic fields are sent raw. Hashing is asynchronous,\n * so the `init` call is deferred until the digests resolve.\n */\nexport function setOpenAIUser(pixelId: string) {\n return ({ user_id, user_data }: UpdateVisitorDTO) => {\n if (typeof window === 'undefined' || !window.oaiq) {\n console.warn('oaiq has not been initialized');\n return;\n }\n // Capture the narrowed reference so the deferred `init` closure keeps it non-optional.\n const oaiq = window.oaiq;\n\n const email = getFirst(user_data?.email)?.trim().toLowerCase();\n const address = getFirst(user_data?.address);\n\n const base: OAIQUser = {\n country: address?.country?.trim().toUpperCase(),\n city: address?.city?.trim().toLowerCase(),\n zip_code: address?.postal_code,\n };\n\n const init = (hashed: Partial<OAIQUser>) => {\n oaiq('init', { pixelId, user: clean({ ...base, ...hashed }) });\n };\n\n Promise.all([email ? sha256(email) : undefined, user_id ? sha256(user_id) : undefined])\n .then(([email_sha256, external_id_sha256]) => init({ email_sha256, external_id_sha256 }))\n .catch(() => init({}));\n };\n}\n"],"mappings":";;;;;AAeA,SAAS,MAAM,OAAqB;CAClC,OAAO,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AACzC;;;;;;AAOA,SAAgB,gBACd,MACA,YACA,SACA;CACA,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,MAAM;EACjD,QAAQ,KAAK,+BAA+B;EAC5C;CACF;CACA,IAAI,cAAc,SAAS,IAAI,GAAG;CAClC,IAAI,OAAO,SAAS,KAAK,SAAS,WAAW,GAAG;CAChD,IAAI,OAAO,SAAS,KAAK,SAAS,WAAW,GAAG;CAEhD,MAAM,EAAE,MAAM,SAAS,YAAY,MAAM,UAAU;CACnD,IAAI,SAAS,UACX,OAAO,KAAK,WAAW,UAAU,MAAM,IAAI,GAAG;EAAE,UAAU;EAAS,mBAAmB;CAAK,CAAC;MAE5F,OAAO,KAAK,WAAW,MAAM,MAAM,IAAI,GAAG,EAAE,UAAU,QAAQ,CAAC;AAEnE;;;;;;AAOA,SAAgB,cAAc,SAAiB;CAC7C,QAAQ,EAAE,SAAS,gBAAkC;EACnD,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,MAAM;GACjD,QAAQ,KAAK,+BAA+B;GAC5C;EACF;EAEA,MAAM,OAAO,OAAO;EAEpB,MAAM,QAAQ,SAAS,WAAW,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC,YAAY;EAC7D,MAAM,UAAU,SAAS,WAAW,OAAO;EAE3C,MAAM,OAAiB;GACrB,SAAS,SAAS,SAAS,KAAK,CAAC,CAAC,YAAY;GAC9C,MAAM,SAAS,MAAM,KAAK,CAAC,CAAC,YAAY;GACxC,UAAU,SAAS;EACrB;EAEA,MAAM,QAAQ,WAA8B;GAC1C,KAAK,QAAQ;IAAE;IAAS,MAAM,MAAM;KAAE,GAAG;KAAM,GAAG;IAAO,CAAC;GAAE,CAAC;EAC/D;EAEA,QAAQ,IAAI,CAAC,QAAQ,OAAO,KAAK,IAAI,KAAA,GAAW,UAAU,OAAO,OAAO,IAAI,KAAA,CAAS,CAAC,CAAC,CACpF,MAAM,CAAC,cAAc,wBAAwB,KAAK;GAAE;GAAc;EAAmB,CAAC,CAAC,CAAC,CACxF,YAAY,KAAK,CAAC,CAAC,CAAC;CACzB;AACF"}
1
+ {"version":3,"file":"openai-pixel.mjs","names":[],"sources":["../../src/third-parties/openai-pixel.ts"],"sourcesContent":["import type { UpdateVisitorDTO } from '../schema/index';\nimport {\n NON_AD_EVENTS,\n type OAIQ,\n type OAIQUser,\n mapOAIEvent,\n normalizeOAIName,\n normalizeOAIPhone,\n oaiCustomEventName,\n} from '../track/oaiq';\nimport type { EventName, TrackName, TrackProperties } from '../track/types';\nimport { getFirst } from '../utils/field';\nimport { sha256 } from '../utils/sha256';\n\ndeclare global {\n interface Window {\n /** Undefined until the OpenAI pixel script has loaded. */\n oaiq?: OAIQ['oaiq'];\n }\n}\n\n/** Drop `undefined` fields so the SDK only receives populated values. */\n// oxlint-disable-next-line @typescript-eslint/no-explicit-any\nfunction clean(value: unknown): any {\n return JSON.parse(JSON.stringify(value));\n}\n\n/**\n * Forward an internal track event to the OpenAI measurement pixel.\n * `eventId` is reused as the OpenAI `event_id` so browser events deduplicate against the\n * Conversions API. https://developers.openai.com/ads/measurement-pixel\n */\nexport function sendOpenAIEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n eventId?: string\n) {\n if (typeof window === 'undefined' || !window.oaiq) {\n console.warn('oaiq has not been initialized');\n return;\n }\n if (NON_AD_EVENTS.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 { type, data } = mapOAIEvent(name, properties);\n if (type === 'custom') {\n // Named as the Conversions API names it, so the two deduplicate; skipped when it cannot be.\n const custom_event_name = oaiCustomEventName(name);\n if (!custom_event_name) return;\n window.oaiq('measure', 'custom', clean(data), { event_id: eventId, custom_event_name });\n } else {\n window.oaiq('measure', type, clean(data), { event_id: eventId });\n }\n}\n\n/**\n * Re-initialize the pixel with hashed user identity for better conversion matching. Email, phone,\n * external id and names are normalized as OpenAI documents and hashed client-side; geographic\n * fields are sent raw. Hashing is asynchronous, so the `init` call is deferred until the digests\n * resolve.\n */\nexport function setOpenAIUser(pixelId: string) {\n return ({ user_id, user_data }: UpdateVisitorDTO) => {\n if (typeof window === 'undefined' || !window.oaiq) {\n console.warn('oaiq has not been initialized');\n return;\n }\n // Capture the narrowed reference so the deferred `init` closure keeps it non-optional.\n const oaiq = window.oaiq;\n\n const email = getFirst(user_data?.email)?.trim().toLowerCase() || undefined;\n const phone = getFirst(user_data?.phone_number);\n const address = getFirst(user_data?.address);\n\n const base: OAIQUser = {\n country: address?.country?.trim().toUpperCase(),\n city: address?.city?.trim(),\n region: address?.region?.trim(),\n postal_code: address?.postal_code?.trim(),\n };\n\n const init = (hashed: Partial<OAIQUser>) => {\n oaiq('init', { pixelId, user: clean({ ...base, ...hashed }) });\n };\n\n const hash = (value: string | undefined) => (value ? sha256(value) : undefined);\n Promise.all([\n hash(email),\n hash(phone ? normalizeOAIPhone(phone) : undefined),\n hash(user_id?.trim()),\n hash(normalizeOAIName(address?.first_name)),\n hash(normalizeOAIName(address?.last_name)),\n ])\n .then(([email_sha256, phone_number_sha256, external_id_sha256, first, last]) =>\n init({\n email_sha256,\n phone_number_sha256,\n external_id_sha256,\n first_name_sha256: first,\n last_name_sha256: last,\n })\n )\n .catch(() => init({}));\n };\n}\n"],"mappings":";;;;;AAuBA,SAAS,MAAM,OAAqB;CAClC,OAAO,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AACzC;;;;;;AAOA,SAAgB,gBACd,MACA,YACA,SACA;CACA,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,MAAM;EACjD,QAAQ,KAAK,+BAA+B;EAC5C;CACF;CACA,IAAI,cAAc,SAAS,IAAI,GAAG;CAClC,IAAI,OAAO,SAAS,KAAK,SAAS,WAAW,GAAG;CAChD,IAAI,OAAO,SAAS,KAAK,SAAS,WAAW,GAAG;CAEhD,MAAM,EAAE,MAAM,SAAS,YAAY,MAAM,UAAU;CACnD,IAAI,SAAS,UAAU;EAErB,MAAM,oBAAoB,mBAAmB,IAAI;EACjD,IAAI,CAAC,mBAAmB;EACxB,OAAO,KAAK,WAAW,UAAU,MAAM,IAAI,GAAG;GAAE,UAAU;GAAS;EAAkB,CAAC;CACxF,OACE,OAAO,KAAK,WAAW,MAAM,MAAM,IAAI,GAAG,EAAE,UAAU,QAAQ,CAAC;AAEnE;;;;;;;AAQA,SAAgB,cAAc,SAAiB;CAC7C,QAAQ,EAAE,SAAS,gBAAkC;EACnD,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,MAAM;GACjD,QAAQ,KAAK,+BAA+B;GAC5C;EACF;EAEA,MAAM,OAAO,OAAO;EAEpB,MAAM,QAAQ,SAAS,WAAW,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC,YAAY,KAAK,KAAA;EAClE,MAAM,QAAQ,SAAS,WAAW,YAAY;EAC9C,MAAM,UAAU,SAAS,WAAW,OAAO;EAE3C,MAAM,OAAiB;GACrB,SAAS,SAAS,SAAS,KAAK,CAAC,CAAC,YAAY;GAC9C,MAAM,SAAS,MAAM,KAAK;GAC1B,QAAQ,SAAS,QAAQ,KAAK;GAC9B,aAAa,SAAS,aAAa,KAAK;EAC1C;EAEA,MAAM,QAAQ,WAA8B;GAC1C,KAAK,QAAQ;IAAE;IAAS,MAAM,MAAM;KAAE,GAAG;KAAM,GAAG;IAAO,CAAC;GAAE,CAAC;EAC/D;EAEA,MAAM,QAAQ,UAA+B,QAAQ,OAAO,KAAK,IAAI,KAAA;EACrE,QAAQ,IAAI;GACV,KAAK,KAAK;GACV,KAAK,QAAQ,kBAAkB,KAAK,IAAI,KAAA,CAAS;GACjD,KAAK,SAAS,KAAK,CAAC;GACpB,KAAK,iBAAiB,SAAS,UAAU,CAAC;GAC1C,KAAK,iBAAiB,SAAS,SAAS,CAAC;EAC3C,CAAC,CAAC,CACC,MAAM,CAAC,cAAc,qBAAqB,oBAAoB,OAAO,UACpE,KAAK;GACH;GACA;GACA;GACA,mBAAmB;GACnB,kBAAkB;EACpB,CAAC,CACH,CAAC,CACA,YAAY,KAAK,CAAC,CAAC,CAAC;CACzB;AACF"}
@@ -39,146 +39,213 @@ async function captureTags() {
39
39
  }
40
40
  }
41
41
  /**
42
- * The `session_start` a failed batch took down with it, kept for the next batch of the same
43
- * session. `fetch` has already retried the transient failures by the time a batch fails here;
44
- * what remains is a batch the server rejected outright — one invalid event fails the whole batch,
45
- * and `session_start` with it — or a network that stayed down past the retries. Neither is a
46
- * reason to lose the session's one attribution record, so it goes out again, with the tags and
47
- * timestamp of the moment the session actually began, under the same session id. Held in memory
48
- * only: a reload loses it, and with it the session_start — the same as before this existed.
42
+ * The events not yet sent, and the only place they are: a batch leaves it at the moment it is
43
+ * handed to `fetch` or to a beacon, never earlier. So when the page is hidden or left,
44
+ * everything still unsent is right here for `sendPendingEvents`.
49
45
  */
50
- let pendingStart;
51
- async function sendEvents(events) {
52
- /** The announcement this batch carries, if any, so the catch below can hold it back. */
53
- let announcement;
46
+ const list = [];
47
+ const batchSize = 10;
48
+ const delay = 2e3;
49
+ let timer = null;
50
+ /**
51
+ * Queues an event, preceded by a `session_start` when it is the one that opens a session.
52
+ *
53
+ * The session is decided here, when the event happens, as GA4 and PostHog decide it: the
54
+ * `session_start` then sits in the queue right before the event that opened the session, so
55
+ * wherever that event goes — a batch, a beacon, a retry — its announcement has gone ahead of it.
56
+ * It carries that event's tags and time, as GA4 derives it from the hit that started the
57
+ * session: the landing page is where the session began, whatever the page has navigated to by
58
+ * the time the batch goes out.
59
+ */
60
+ function enqueue(name, properties, options) {
61
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
62
+ const tags = captureTags();
63
+ const { id: session_id, started } = require_setup_session.getSession().touch(Date.parse(timestamp));
64
+ const item = (fields) => {
65
+ const result = {
66
+ ...fields,
67
+ tags,
68
+ timestamp,
69
+ session_id
70
+ };
71
+ tags.then((settled) => result.settled = settled);
72
+ return result;
73
+ };
74
+ if (started) list.push(item({
75
+ name: "session_start",
76
+ properties: {},
77
+ options: { enableThirdPartyTracking: false }
78
+ }));
79
+ const event = item({
80
+ name,
81
+ properties,
82
+ options
83
+ });
84
+ list.push(event);
85
+ return event;
86
+ }
87
+ /**
88
+ * Sends the queue. Everything that has to be awaited is awaited first, and only then are the
89
+ * events taken off the queue and handed to `fetch` in the same synchronous step — so there is
90
+ * no moment at which an event is neither queued nor sent.
91
+ */
92
+ async function flush() {
93
+ if (timer) {
94
+ clearTimeout(timer);
95
+ timer = null;
96
+ }
97
+ if (list.length === 0) return;
98
+ let events = [];
54
99
  try {
55
- if (events.length === 0) return;
56
- const opening = events[0];
57
- const { id: session_id, started } = require_setup_session.getSession().touch(Date.parse(opening.timestamp), Date.parse(events[events.length - 1].timestamp));
58
- if (started || pendingStart?.session_id === session_id) {
59
- const start = pendingStart?.session_id === session_id ? pendingStart.item : {
60
- name: "session_start",
61
- properties: {},
62
- options: { enableThirdPartyTracking: false },
63
- tags: opening.tags,
64
- timestamp: opening.timestamp
65
- };
66
- pendingStart = void 0;
67
- announcement = {
68
- session_id,
69
- item: start
70
- };
71
- events.unshift(start);
72
- }
73
100
  await getTokenBucket().removeTokens();
74
101
  const visitor_id = (await require_visitor_index.getVisitor()).id;
102
+ const headers = await require_setup_index.config.getHeaders();
103
+ await Promise.all(list.map((event) => event.tags));
104
+ events = list.splice(0);
105
+ if (events.length === 0) return;
75
106
  const dto = await Promise.all(events.map(async (event) => ({
76
107
  name: event.name,
77
108
  properties: event.properties,
78
109
  tags: await event.tags,
79
110
  visitor_id,
80
- session_id,
111
+ session_id: event.session_id,
81
112
  platform: require_setup_index.config.platform,
82
113
  environment: require_setup_index.config.environment,
83
114
  timestamp: event.timestamp
84
115
  })));
116
+ const body = JSON.stringify(dto);
85
117
  const response = await (0, _shware_utils.fetch)(`${require_setup_index.config.endpoint}/events`, {
86
118
  method: "POST",
87
119
  credentials: "include",
88
- keepalive: true,
89
- headers: await require_setup_index.config.getHeaders(),
90
- body: JSON.stringify(dto)
120
+ keepalive: body.length < 6e4,
121
+ headers,
122
+ body
91
123
  });
92
124
  if (!response.ok) throw new Error(`Failed to send track event: ${response.status} ${await response.text()}`);
93
125
  const data = await response.json();
94
- let index = 0;
95
- while (events.length > 0) {
96
- const event = events.shift();
97
- if (!event) {
98
- index++;
99
- continue;
100
- }
101
- const { options, name, properties } = event;
126
+ events.forEach((event, index) => {
102
127
  const eventId = data.at(index)?.id;
103
- options.onSucceed?.(eventId ? { id: eventId } : void 0);
104
- index++;
105
- if (options.enableThirdPartyTracking === false || require_third_parties_ignored_events.IGNORED_EVENTS.includes(name)) continue;
128
+ event.options.onSucceed?.(eventId ? { id: eventId } : void 0);
129
+ if (event.options.enableThirdPartyTracking === false || require_third_parties_ignored_events.IGNORED_EVENTS.includes(event.name)) return;
106
130
  require_setup_index.config.thirdPartyTrackers.forEach((tracker) => {
107
131
  try {
108
- tracker(name, properties, eventId);
132
+ tracker(event.name, event.properties, eventId);
109
133
  } catch (e) {
110
134
  if (e instanceof Error) console.log(e.message);
111
135
  }
112
136
  });
113
- }
137
+ });
114
138
  } catch (e) {
115
139
  if (e instanceof Error) console.log(e.message);
116
- if (announcement) pendingStart = announcement;
117
- events.forEach((event) => event.options.onError?.(e));
118
- }
119
- }
120
- const batch = 10;
121
- const delay = 2e3;
122
- const list = [];
123
- let timer = null;
124
- /**
125
- * Both paths into a send go through here, so a batch that fills up cancels the timer the
126
- * previous push armed rather than leaving it to wake up on its own with nothing to send.
127
- */
128
- function flush() {
129
- if (timer) {
130
- clearTimeout(timer);
131
- timer = null;
140
+ const starts = events.filter((event) => event.name === "session_start");
141
+ list.unshift(...starts);
142
+ events.filter((event) => event.name !== "session_start").forEach((event) => event.options.onError?.(e));
132
143
  }
133
- if (list.length === 0) return;
134
- const copy = [...list];
135
- list.length = 0;
136
- sendEvents(copy);
137
144
  }
138
145
  function track(name, properties, options = defaultOptions) {
139
- list.push({
140
- name,
141
- properties,
142
- options,
143
- tags: captureTags(),
144
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
145
- });
146
- if (list.length >= batch) {
146
+ enqueue(name, properties, options);
147
+ if (list.length >= batchSize) {
147
148
  flush();
148
149
  return;
149
150
  }
150
151
  if (timer) clearTimeout(timer);
151
- timer = setTimeout(flush, delay);
152
+ timer = setTimeout(() => void flush(), delay);
152
153
  }
154
+ /**
155
+ * Sends the event now, with whatever is queued, and resolves once it has been sent or lost — or,
156
+ * when no visitor could be created to send it with, at once, the event left queued for the next
157
+ * send. It never rejects: tracking must not fail the step that awaits it.
158
+ */
153
159
  async function trackAsync(name, properties, options = defaultOptions) {
154
- await sendEvents([{
155
- name,
156
- properties,
157
- options,
158
- tags: captureTags(),
159
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
160
- }]);
160
+ let settle;
161
+ const settled = new Promise((resolve) => {
162
+ settle = resolve;
163
+ });
164
+ const event = enqueue(name, properties, {
165
+ ...options,
166
+ onSucceed: (response) => {
167
+ options.onSucceed?.(response);
168
+ settle();
169
+ },
170
+ onError: (error) => {
171
+ options.onError?.(error);
172
+ settle();
173
+ }
174
+ });
175
+ await flush();
176
+ if (list.includes(event)) return;
177
+ await settled;
161
178
  }
162
- function sendBeacon(name, properties) {
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() {
163
187
  const stored = require_setup_index.config.storage.getItem(require_constants_storage.keys.visitor_id);
164
- const visitor_id = require_setup_index.cache.visitor?.id ?? (stored && stored !== "undefined" ? stored : void 0);
188
+ return require_setup_index.cache.visitor?.id ?? (stored && stored !== "undefined" ? stored : void 0);
189
+ }
190
+ function beacon(dto) {
191
+ if (typeof navigator === "undefined" || !("sendBeacon" in navigator)) return false;
192
+ const blob = new Blob([JSON.stringify(dto)], { type: "text/plain;charset=UTF-8" });
193
+ return navigator.sendBeacon(`${require_setup_index.config.endpoint}/events`, blob);
194
+ }
195
+ /**
196
+ * Sends the queue by beacon, now: for when the page is hidden or left, the last moment its
197
+ * script is sure to run — what PostHog's `requestQueue.unload()` and GA4 do on the same events.
198
+ * Call it before the engagement beacon: a new session's `session_start` is in this queue.
199
+ *
200
+ * Nothing can be awaited here, so the events go with the tags they have settled on (the last
201
+ * 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. When no beacon can
203
+ * take them — a visitor the server does not know yet, no `sendBeacon`, a refused body — they stay
204
+ * queued for the usual send, which also creates the visitor.
205
+ */
206
+ function sendPendingEvents() {
207
+ if (list.length === 0) return;
208
+ const visitor_id = beaconVisitorId();
209
+ const dto = list.map((event) => ({
210
+ name: event.name,
211
+ properties: event.properties,
212
+ tags: event.settled ?? require_setup_index.cache.tags ?? {},
213
+ visitor_id: visitor_id ?? "",
214
+ session_id: event.session_id,
215
+ platform: require_setup_index.config.platform,
216
+ environment: require_setup_index.config.environment,
217
+ timestamp: event.timestamp
218
+ }));
219
+ if (!visitor_id || !beacon(dto)) {
220
+ flush();
221
+ return;
222
+ }
223
+ if (timer) {
224
+ clearTimeout(timer);
225
+ timer = null;
226
+ }
227
+ list.splice(0).forEach((event) => event.options.onSucceed?.(void 0));
228
+ }
229
+ function sendBeacon(name, properties) {
230
+ const visitor_id = beaconVisitorId();
165
231
  if (!visitor_id) return;
166
- const dto = [{
232
+ const session_id = require_setup_session.getSession().extend();
233
+ if (!session_id) return;
234
+ if (beacon([{
167
235
  name,
168
236
  properties,
169
237
  tags: require_setup_index.cache.tags ?? {},
170
238
  visitor_id,
171
- session_id: require_setup_session.getSession().extend(),
239
+ session_id,
172
240
  platform: require_setup_index.config.platform,
173
241
  environment: require_setup_index.config.environment,
174
242
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
175
- }];
176
- const blob = new Blob([JSON.stringify(dto)], { type: "application/json" });
177
- if (navigator.sendBeacon(`${require_setup_index.config.endpoint}/events`, blob)) return;
243
+ }])) return;
178
244
  console.warn("Failed to send beacon", name, properties);
179
245
  }
180
246
  //#endregion
181
247
  exports.sendBeacon = sendBeacon;
248
+ exports.sendPendingEvents = sendPendingEvents;
182
249
  exports.track = track;
183
250
  exports.trackAsync = trackAsync;
184
251
 
@@ -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 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 `session_start` a failed batch took down with it, kept for the next batch of the same\n * session. `fetch` has already retried the transient failures by the time a batch fails here;\n * what remains is a batch the server rejected outright — one invalid event fails the whole batch,\n * and `session_start` with it — or a network that stayed down past the retries. Neither is a\n * reason to lose the session's one attribution record, so it goes out again, with the tags and\n * timestamp of the moment the session actually began, under the same session id. Held in memory\n * only: a reload loses it, and with it the session_start — the same as before this existed.\n */\nlet pendingStart: { session_id: string; item: Item } | undefined;\n\nasync function sendEvents(events: Item[]) {\n /** The announcement this batch carries, if any, so the catch below can hold it back. */\n let announcement: { session_id: string; item: Item } | undefined;\n try {\n if (events.length === 0) return;\n\n // One read-modify-write of the stored session for the whole batch: it answers which session\n // these events belong to and whether this batch is the one that started it. Timed by the\n // events themselves rather than by this moment — a tab frozen in the background can hold a\n // batch for far longer than `delay`, and those events belong to the session they happened in.\n const opening = events[0];\n const { id: session_id, started } = getSession().touch(\n Date.parse(opening.timestamp),\n Date.parse(events[events.length - 1].timestamp)\n );\n if (started || pendingStart?.session_id === session_id) {\n // `session_start` is derived from the event that opened the session, as GA4 derives it\n // from the hit that did: when that event happened is when the session began, and the page\n // it was captured on is where. Neither is read now — this batch goes out up to `delay` ms\n // later, a batch held in a frozen tab far later still, and a landing page that redirects\n // inside that window would stamp the session's one attribution record with the URL the utm\n // parameters were already stripped from.\n const start: Item =\n pendingStart?.session_id === session_id\n ? pendingStart.item\n : {\n name: 'session_start',\n properties: {},\n options: { enableThirdPartyTracking: false },\n tags: opening.tags,\n timestamp: opening.timestamp,\n };\n pendingStart = undefined;\n announcement = { session_id, item: start };\n events.unshift(start);\n }\n\n await getTokenBucket().removeTokens();\n\n const visitor_id = (await getVisitor()).id;\n\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,\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }))\n );\n\n const response = await fetch(`${config.endpoint}/events`, {\n method: 'POST',\n credentials: 'include',\n // Survive the page being unloaded mid-flight: a batch waits up to `delay` ms, so closing\n // the tab inside that window would otherwise abort the request and lose every event in it.\n // The body stays far under keepalive's 64KB in-flight budget at 100 events per batch.\n keepalive: true,\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\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 let index = 0;\n while (events.length > 0) {\n const event = events.shift();\n if (!event) {\n index++;\n continue;\n }\n const { options, name, properties } = event;\n const eventId = data.at(index)?.id;\n options.onSucceed?.(eventId ? { id: eventId } : undefined);\n index++;\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 (options.enableThirdPartyTracking === false || IGNORED_EVENTS.includes(name)) {\n continue;\n }\n config.thirdPartyTrackers.forEach((tracker) => {\n try {\n tracker(name, properties, eventId);\n } catch (e: unknown) {\n // A third-party script does not get to take the rest of the batch with it. This loop is\n // still draining `events` with `shift`, so a throw would escape to the catch below and\n // report failure to whatever is left in the queue — for events the server has already\n // accepted, and after the ones ahead of them were told they succeeded.\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 // The other events are reported lost and stay lost; `session_start` alone is worth carrying\n // over, because it is the session's only attribution record — a session without one has no\n // channel, and its every later event drops out of an attribution join.\n if (announcement) pendingStart = announcement;\n events.forEach((event) => event.options.onError?.(e));\n }\n}\n\nconst batch = 10;\nconst delay = 2000;\nconst list: Item[] = [];\nlet timer: ReturnType<typeof setTimeout> | null = null;\n\n/**\n * Both paths into a send go through here, so a batch that fills up cancels the timer the\n * previous push armed rather than leaving it to wake up on its own with nothing to send.\n */\nfunction flush() {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n if (list.length === 0) return;\n const copy = [...list];\n list.length = 0;\n void sendEvents(copy);\n}\n\nexport function track<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n list.push({\n name,\n properties,\n options,\n tags: captureTags(),\n timestamp: new Date().toISOString(),\n });\n if (list.length >= batch) {\n flush();\n return;\n }\n if (timer) clearTimeout(timer);\n timer = setTimeout(flush, delay);\n}\n\nexport async function trackAsync<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n await sendEvents([\n { name, properties, options, tags: captureTags(), timestamp: new Date().toISOString() },\n ]);\n}\n\nexport function sendBeacon<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n) {\n // The visitor id is persisted, so a returning visitor already has one before `getVisitor` has\n // finished its round trip for this page. Requiring the in-memory copy threw away exactly the\n // events this function exists for: everything a visit accrues before its first batch comes\n // back, which for a short visit is the whole of it.\n const stored = config.storage.getItem(keys.visitor_id);\n const visitor_id = cache.visitor?.id ?? (stored && stored !== 'undefined' ? stored : undefined);\n if (!visitor_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: getSession().extend(),\n platform: config.platform,\n environment: config.environment,\n timestamp: new Date().toISOString(),\n },\n ];\n const blob = new Blob([JSON.stringify(dto)], { type: 'application/json' });\n const success = navigator.sendBeacon(`${config.endpoint}/events`, blob);\n if (success) 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","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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"}
@@ -6,7 +6,24 @@ export interface TrackOptions {
6
6
  onError?: (error: unknown) => void;
7
7
  }
8
8
  export declare function track<T extends EventName = EventName>(name: TrackName<T>, properties?: TrackProperties<T>, options?: TrackOptions): void;
9
+ /**
10
+ * Sends the event now, with whatever is queued, and resolves once it has been sent or lost — or,
11
+ * when no visitor could be created to send it with, at once, the event left queued for the next
12
+ * send. It never rejects: tracking must not fail the step that awaits it.
13
+ */
9
14
  export declare function trackAsync<T extends EventName = EventName>(name: TrackName<T>, properties?: TrackProperties<T>, options?: TrackOptions): Promise<void>;
15
+ /**
16
+ * Sends the queue by beacon, now: for when the page is hidden or left, the last moment its
17
+ * script is sure to run — what PostHog's `requestQueue.unload()` and GA4 do on the same events.
18
+ * Call it before the engagement beacon: a new session's `session_start` is in this queue.
19
+ *
20
+ * Nothing can be awaited here, so the events go with the tags they have settled on (the last
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. When no beacon can
23
+ * take them — a visitor the server does not know yet, no `sendBeacon`, a refused body — they stay
24
+ * queued for the usual send, which also creates the visitor.
25
+ */
26
+ export declare function sendPendingEvents(): void;
10
27
  export declare function sendBeacon<T extends EventName = EventName>(name: TrackName<T>, properties?: TrackProperties<T>): void;
11
28
  //#endregion
12
29
  //# sourceMappingURL=index.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/track/index.ts"],"mappings":";;iBASiB;EACf;EACA,aAAa,WAAW;EACxB,WAAW;;wBAyLG,MAAM,UAAU,YAAY,WAC1C,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS;wBAiBW,WAAW,UAAU,YAAY,WACrD,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS,eAA6B;wBAOxB,WAAW,UAAU,YAAY,WAC/C,MAAM,UAAU,IAChB,aAAa,gBAAgB"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/track/index.ts"],"mappings":";;iBASiB;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;;;;;;;;;;;;wBAyDxB;wBAwBA,WAAW,UAAU,YAAY,WAC/C,MAAM,UAAU,IAChB,aAAa,gBAAgB"}
@@ -6,7 +6,24 @@ export interface TrackOptions {
6
6
  onError?: (error: unknown) => void;
7
7
  }
8
8
  export declare function track<T extends EventName = EventName>(name: TrackName<T>, properties?: TrackProperties<T>, options?: TrackOptions): void;
9
+ /**
10
+ * Sends the event now, with whatever is queued, and resolves once it has been sent or lost — or,
11
+ * when no visitor could be created to send it with, at once, the event left queued for the next
12
+ * send. It never rejects: tracking must not fail the step that awaits it.
13
+ */
9
14
  export declare function trackAsync<T extends EventName = EventName>(name: TrackName<T>, properties?: TrackProperties<T>, options?: TrackOptions): Promise<void>;
15
+ /**
16
+ * Sends the queue by beacon, now: for when the page is hidden or left, the last moment its
17
+ * script is sure to run — what PostHog's `requestQueue.unload()` and GA4 do on the same events.
18
+ * Call it before the engagement beacon: a new session's `session_start` is in this queue.
19
+ *
20
+ * Nothing can be awaited here, so the events go with the tags they have settled on (the last
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. When no beacon can
23
+ * take them — a visitor the server does not know yet, no `sendBeacon`, a refused body — they stay
24
+ * queued for the usual send, which also creates the visitor.
25
+ */
26
+ export declare function sendPendingEvents(): void;
10
27
  export declare function sendBeacon<T extends EventName = EventName>(name: TrackName<T>, properties?: TrackProperties<T>): void;
11
28
  //#endregion
12
29
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/track/index.ts"],"mappings":";;iBASiB;EACf;EACA,aAAa,WAAW;EACxB,WAAW;;wBAyLG,MAAM,UAAU,YAAY,WAC1C,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS;wBAiBW,WAAW,UAAU,YAAY,WACrD,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS,eAA6B;wBAOxB,WAAW,UAAU,YAAY,WAC/C,MAAM,UAAU,IAChB,aAAa,gBAAgB"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/track/index.ts"],"mappings":";;iBASiB;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;;;;;;;;;;;;wBAyDxB;wBAwBA,WAAW,UAAU,YAAY,WAC/C,MAAM,UAAU,IAChB,aAAa,gBAAgB"}