@shware/analytics 8.16.0 → 9.0.1

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 (116) hide show
  1. package/README.md +28 -4
  2. package/dist/attribution/classify.cjs +6 -6
  3. package/dist/attribution/classify.cjs.map +1 -1
  4. package/dist/attribution/classify.mjs +6 -6
  5. package/dist/attribution/classify.mjs.map +1 -1
  6. package/dist/attribution/vocabulary.cjs +3 -4
  7. package/dist/attribution/vocabulary.cjs.map +1 -1
  8. package/dist/attribution/vocabulary.d.cts +3 -4
  9. package/dist/attribution/vocabulary.d.cts.map +1 -1
  10. package/dist/attribution/vocabulary.d.mts +3 -4
  11. package/dist/attribution/vocabulary.d.mts.map +1 -1
  12. package/dist/attribution/vocabulary.mjs +3 -4
  13. package/dist/attribution/vocabulary.mjs.map +1 -1
  14. package/dist/click-id/index.cjs +24 -6
  15. package/dist/click-id/index.cjs.map +1 -1
  16. package/dist/click-id/index.d.cts +14 -3
  17. package/dist/click-id/index.d.cts.map +1 -1
  18. package/dist/click-id/index.d.mts +14 -3
  19. package/dist/click-id/index.d.mts.map +1 -1
  20. package/dist/click-id/index.mjs +23 -7
  21. package/dist/click-id/index.mjs.map +1 -1
  22. package/dist/hooks/use-web-analytics.cjs +5 -1
  23. package/dist/hooks/use-web-analytics.cjs.map +1 -1
  24. package/dist/hooks/use-web-analytics.d.cts.map +1 -1
  25. package/dist/hooks/use-web-analytics.d.mts.map +1 -1
  26. package/dist/hooks/use-web-analytics.mjs +6 -2
  27. package/dist/hooks/use-web-analytics.mjs.map +1 -1
  28. package/dist/index.cjs +1 -0
  29. package/dist/index.d.cts +2 -2
  30. package/dist/index.d.mts +2 -2
  31. package/dist/index.mjs +2 -2
  32. package/dist/schema/index.cjs +25 -9
  33. package/dist/schema/index.cjs.map +1 -1
  34. package/dist/schema/index.d.cts +114 -54
  35. package/dist/schema/index.d.cts.map +1 -1
  36. package/dist/schema/index.d.mts +114 -54
  37. package/dist/schema/index.d.mts.map +1 -1
  38. package/dist/schema/index.mjs +25 -9
  39. package/dist/schema/index.mjs.map +1 -1
  40. package/dist/server/click-ids.cjs +93 -0
  41. package/dist/server/click-ids.cjs.map +1 -0
  42. package/dist/server/click-ids.d.cts +40 -0
  43. package/dist/server/click-ids.d.cts.map +1 -0
  44. package/dist/server/click-ids.d.mts +40 -0
  45. package/dist/server/click-ids.d.mts.map +1 -0
  46. package/dist/server/click-ids.mjs +85 -0
  47. package/dist/server/click-ids.mjs.map +1 -0
  48. package/dist/server/google-data-manager.cjs +2 -1
  49. package/dist/server/google-data-manager.cjs.map +1 -1
  50. package/dist/server/google-data-manager.d.cts.map +1 -1
  51. package/dist/server/google-data-manager.d.mts.map +1 -1
  52. package/dist/server/google-data-manager.mjs +2 -1
  53. package/dist/server/google-data-manager.mjs.map +1 -1
  54. package/dist/server/linkedin-conversions-api.cjs +9 -4
  55. package/dist/server/linkedin-conversions-api.cjs.map +1 -1
  56. package/dist/server/linkedin-conversions-api.d.cts.map +1 -1
  57. package/dist/server/linkedin-conversions-api.d.mts.map +1 -1
  58. package/dist/server/linkedin-conversions-api.mjs +9 -4
  59. package/dist/server/linkedin-conversions-api.mjs.map +1 -1
  60. package/dist/server/meta-capi.cjs +5 -4
  61. package/dist/server/meta-capi.cjs.map +1 -1
  62. package/dist/server/meta-capi.mjs +5 -4
  63. package/dist/server/meta-capi.mjs.map +1 -1
  64. package/dist/server/meta-conversions-api.cjs +5 -4
  65. package/dist/server/meta-conversions-api.cjs.map +1 -1
  66. package/dist/server/meta-conversions-api.d.cts.map +1 -1
  67. package/dist/server/meta-conversions-api.d.mts.map +1 -1
  68. package/dist/server/meta-conversions-api.mjs +5 -4
  69. package/dist/server/meta-conversions-api.mjs.map +1 -1
  70. package/dist/server/microsoft-conversions-api.cjs +3 -1
  71. package/dist/server/microsoft-conversions-api.cjs.map +1 -1
  72. package/dist/server/microsoft-conversions-api.d.cts.map +1 -1
  73. package/dist/server/microsoft-conversions-api.d.mts.map +1 -1
  74. package/dist/server/microsoft-conversions-api.mjs +3 -1
  75. package/dist/server/microsoft-conversions-api.mjs.map +1 -1
  76. package/dist/server/openai-conversions-api.cjs +33 -11
  77. package/dist/server/openai-conversions-api.cjs.map +1 -1
  78. package/dist/server/openai-conversions-api.d.cts +19 -8
  79. package/dist/server/openai-conversions-api.d.cts.map +1 -1
  80. package/dist/server/openai-conversions-api.d.mts +19 -8
  81. package/dist/server/openai-conversions-api.d.mts.map +1 -1
  82. package/dist/server/openai-conversions-api.mjs +33 -11
  83. package/dist/server/openai-conversions-api.mjs.map +1 -1
  84. package/dist/server/reddit-conversions-api.cjs +3 -2
  85. package/dist/server/reddit-conversions-api.cjs.map +1 -1
  86. package/dist/server/reddit-conversions-api.d.cts.map +1 -1
  87. package/dist/server/reddit-conversions-api.d.mts.map +1 -1
  88. package/dist/server/reddit-conversions-api.mjs +3 -2
  89. package/dist/server/reddit-conversions-api.mjs.map +1 -1
  90. package/dist/setup/session.cjs +1 -1
  91. package/dist/setup/session.cjs.map +1 -1
  92. package/dist/setup/session.d.cts +6 -5
  93. package/dist/setup/session.d.cts.map +1 -1
  94. package/dist/setup/session.d.mts +6 -5
  95. package/dist/setup/session.d.mts.map +1 -1
  96. package/dist/setup/session.mjs +1 -1
  97. package/dist/setup/session.mjs.map +1 -1
  98. package/dist/track/index.cjs +156 -89
  99. package/dist/track/index.cjs.map +1 -1
  100. package/dist/track/index.d.cts +17 -0
  101. package/dist/track/index.d.cts.map +1 -1
  102. package/dist/track/index.d.mts +17 -0
  103. package/dist/track/index.d.mts.map +1 -1
  104. package/dist/track/index.mjs +156 -90
  105. package/dist/track/index.mjs.map +1 -1
  106. package/dist/track/types.d.cts +48 -16
  107. package/dist/track/types.d.cts.map +1 -1
  108. package/dist/track/types.d.mts +48 -16
  109. package/dist/track/types.d.mts.map +1 -1
  110. package/dist/web/index.cjs +17 -11
  111. package/dist/web/index.cjs.map +1 -1
  112. package/dist/web/index.d.cts.map +1 -1
  113. package/dist/web/index.d.mts.map +1 -1
  114. package/dist/web/index.mjs +17 -11
  115. package/dist/web/index.mjs.map +1 -1
  116. package/package.json +1 -1
@@ -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"}
@@ -38,145 +38,211 @@ async function captureTags() {
38
38
  }
39
39
  }
40
40
  /**
41
- * The `session_start` a failed batch took down with it, kept for the next batch of the same
42
- * session. `fetch` has already retried the transient failures by the time a batch fails here;
43
- * what remains is a batch the server rejected outright — one invalid event fails the whole batch,
44
- * and `session_start` with it — or a network that stayed down past the retries. Neither is a
45
- * reason to lose the session's one attribution record, so it goes out again, with the tags and
46
- * timestamp of the moment the session actually began, under the same session id. Held in memory
47
- * only: a reload loses it, and with it the session_start — the same as before this existed.
41
+ * The events not yet sent, and the only place they are: a batch leaves it at the moment it is
42
+ * handed to `fetch` or to a beacon, never earlier. So when the page is hidden or left,
43
+ * everything still unsent is right here for `sendPendingEvents`.
48
44
  */
49
- let pendingStart;
50
- async function sendEvents(events) {
51
- /** The announcement this batch carries, if any, so the catch below can hold it back. */
52
- let announcement;
45
+ const list = [];
46
+ const batchSize = 10;
47
+ const delay = 2e3;
48
+ let timer = null;
49
+ /**
50
+ * Queues an event, preceded by a `session_start` when it is the one that opens a session.
51
+ *
52
+ * The session is decided here, when the event happens, as GA4 and PostHog decide it: the
53
+ * `session_start` then sits in the queue right before the event that opened the session, so
54
+ * wherever that event goes — a batch, a beacon, a retry — its announcement has gone ahead of it.
55
+ * It carries that event's tags and time, as GA4 derives it from the hit that started the
56
+ * session: the landing page is where the session began, whatever the page has navigated to by
57
+ * the time the batch goes out.
58
+ */
59
+ function enqueue(name, properties, options) {
60
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
61
+ const tags = captureTags();
62
+ const { id: session_id, started } = getSession().touch(Date.parse(timestamp));
63
+ const item = (fields) => {
64
+ const result = {
65
+ ...fields,
66
+ tags,
67
+ timestamp,
68
+ session_id
69
+ };
70
+ tags.then((settled) => result.settled = settled);
71
+ return result;
72
+ };
73
+ if (started) list.push(item({
74
+ name: "session_start",
75
+ properties: {},
76
+ options: { enableThirdPartyTracking: false }
77
+ }));
78
+ const event = item({
79
+ name,
80
+ properties,
81
+ options
82
+ });
83
+ list.push(event);
84
+ return event;
85
+ }
86
+ /**
87
+ * Sends the queue. Everything that has to be awaited is awaited first, and only then are the
88
+ * events taken off the queue and handed to `fetch` in the same synchronous step — so there is
89
+ * no moment at which an event is neither queued nor sent.
90
+ */
91
+ async function flush() {
92
+ if (timer) {
93
+ clearTimeout(timer);
94
+ timer = null;
95
+ }
96
+ if (list.length === 0) return;
97
+ let events = [];
53
98
  try {
54
- if (events.length === 0) return;
55
- const opening = events[0];
56
- const { id: session_id, started } = getSession().touch(Date.parse(opening.timestamp), Date.parse(events[events.length - 1].timestamp));
57
- if (started || pendingStart?.session_id === session_id) {
58
- const start = pendingStart?.session_id === session_id ? pendingStart.item : {
59
- name: "session_start",
60
- properties: {},
61
- options: { enableThirdPartyTracking: false },
62
- tags: opening.tags,
63
- timestamp: opening.timestamp
64
- };
65
- pendingStart = void 0;
66
- announcement = {
67
- session_id,
68
- item: start
69
- };
70
- events.unshift(start);
71
- }
72
99
  await getTokenBucket().removeTokens();
73
100
  const visitor_id = (await getVisitor()).id;
101
+ const headers = await config.getHeaders();
102
+ await Promise.all(list.map((event) => event.tags));
103
+ events = list.splice(0);
104
+ if (events.length === 0) return;
74
105
  const dto = await Promise.all(events.map(async (event) => ({
75
106
  name: event.name,
76
107
  properties: event.properties,
77
108
  tags: await event.tags,
78
109
  visitor_id,
79
- session_id,
110
+ session_id: event.session_id,
80
111
  platform: config.platform,
81
112
  environment: config.environment,
82
113
  timestamp: event.timestamp
83
114
  })));
115
+ const body = JSON.stringify(dto);
84
116
  const response = await fetch(`${config.endpoint}/events`, {
85
117
  method: "POST",
86
118
  credentials: "include",
87
- keepalive: true,
88
- headers: await config.getHeaders(),
89
- body: JSON.stringify(dto)
119
+ keepalive: body.length < 6e4,
120
+ headers,
121
+ body
90
122
  });
91
123
  if (!response.ok) throw new Error(`Failed to send track event: ${response.status} ${await response.text()}`);
92
124
  const data = await response.json();
93
- let index = 0;
94
- while (events.length > 0) {
95
- const event = events.shift();
96
- if (!event) {
97
- index++;
98
- continue;
99
- }
100
- const { options, name, properties } = event;
125
+ events.forEach((event, index) => {
101
126
  const eventId = data.at(index)?.id;
102
- options.onSucceed?.(eventId ? { id: eventId } : void 0);
103
- index++;
104
- if (options.enableThirdPartyTracking === false || IGNORED_EVENTS.includes(name)) continue;
127
+ event.options.onSucceed?.(eventId ? { id: eventId } : void 0);
128
+ if (event.options.enableThirdPartyTracking === false || IGNORED_EVENTS.includes(event.name)) return;
105
129
  config.thirdPartyTrackers.forEach((tracker) => {
106
130
  try {
107
- tracker(name, properties, eventId);
131
+ tracker(event.name, event.properties, eventId);
108
132
  } catch (e) {
109
133
  if (e instanceof Error) console.log(e.message);
110
134
  }
111
135
  });
112
- }
136
+ });
113
137
  } catch (e) {
114
138
  if (e instanceof Error) console.log(e.message);
115
- if (announcement) pendingStart = announcement;
116
- events.forEach((event) => event.options.onError?.(e));
117
- }
118
- }
119
- const batch = 10;
120
- const delay = 2e3;
121
- const list = [];
122
- let timer = null;
123
- /**
124
- * Both paths into a send go through here, so a batch that fills up cancels the timer the
125
- * previous push armed rather than leaving it to wake up on its own with nothing to send.
126
- */
127
- function flush() {
128
- if (timer) {
129
- clearTimeout(timer);
130
- timer = null;
139
+ const starts = events.filter((event) => event.name === "session_start");
140
+ list.unshift(...starts);
141
+ events.filter((event) => event.name !== "session_start").forEach((event) => event.options.onError?.(e));
131
142
  }
132
- if (list.length === 0) return;
133
- const copy = [...list];
134
- list.length = 0;
135
- sendEvents(copy);
136
143
  }
137
144
  function track(name, properties, options = defaultOptions) {
138
- list.push({
139
- name,
140
- properties,
141
- options,
142
- tags: captureTags(),
143
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
144
- });
145
- if (list.length >= batch) {
145
+ enqueue(name, properties, options);
146
+ if (list.length >= batchSize) {
146
147
  flush();
147
148
  return;
148
149
  }
149
150
  if (timer) clearTimeout(timer);
150
- timer = setTimeout(flush, delay);
151
+ timer = setTimeout(() => void flush(), delay);
151
152
  }
153
+ /**
154
+ * Sends the event now, with whatever is queued, and resolves once it has been sent or lost — or,
155
+ * when no visitor could be created to send it with, at once, the event left queued for the next
156
+ * send. It never rejects: tracking must not fail the step that awaits it.
157
+ */
152
158
  async function trackAsync(name, properties, options = defaultOptions) {
153
- await sendEvents([{
154
- name,
155
- properties,
156
- options,
157
- tags: captureTags(),
158
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
159
- }]);
159
+ let settle;
160
+ const settled = new Promise((resolve) => {
161
+ settle = resolve;
162
+ });
163
+ const event = enqueue(name, properties, {
164
+ ...options,
165
+ onSucceed: (response) => {
166
+ options.onSucceed?.(response);
167
+ settle();
168
+ },
169
+ onError: (error) => {
170
+ options.onError?.(error);
171
+ settle();
172
+ }
173
+ });
174
+ await flush();
175
+ if (list.includes(event)) return;
176
+ await settled;
160
177
  }
161
- function sendBeacon(name, properties) {
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() {
162
186
  const stored = config.storage.getItem(keys.visitor_id);
163
- const visitor_id = cache.visitor?.id ?? (stored && stored !== "undefined" ? stored : void 0);
187
+ return cache.visitor?.id ?? (stored && stored !== "undefined" ? stored : void 0);
188
+ }
189
+ function beacon(dto) {
190
+ if (typeof navigator === "undefined" || !("sendBeacon" in navigator)) return false;
191
+ const blob = new Blob([JSON.stringify(dto)], { type: "text/plain;charset=UTF-8" });
192
+ return navigator.sendBeacon(`${config.endpoint}/events`, blob);
193
+ }
194
+ /**
195
+ * Sends the queue by beacon, now: for when the page is hidden or left, the last moment its
196
+ * script is sure to run — what PostHog's `requestQueue.unload()` and GA4 do on the same events.
197
+ * Call it before the engagement beacon: a new session's `session_start` is in this queue.
198
+ *
199
+ * Nothing can be awaited here, so the events go with the tags they have settled on (the last
200
+ * 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. When no beacon can
202
+ * take them — a visitor the server does not know yet, no `sendBeacon`, a refused body — they stay
203
+ * queued for the usual send, which also creates the visitor.
204
+ */
205
+ function sendPendingEvents() {
206
+ if (list.length === 0) return;
207
+ const visitor_id = beaconVisitorId();
208
+ const dto = list.map((event) => ({
209
+ name: event.name,
210
+ properties: event.properties,
211
+ tags: event.settled ?? cache.tags ?? {},
212
+ visitor_id: visitor_id ?? "",
213
+ session_id: event.session_id,
214
+ platform: config.platform,
215
+ environment: config.environment,
216
+ timestamp: event.timestamp
217
+ }));
218
+ if (!visitor_id || !beacon(dto)) {
219
+ flush();
220
+ return;
221
+ }
222
+ if (timer) {
223
+ clearTimeout(timer);
224
+ timer = null;
225
+ }
226
+ list.splice(0).forEach((event) => event.options.onSucceed?.(void 0));
227
+ }
228
+ function sendBeacon(name, properties) {
229
+ const visitor_id = beaconVisitorId();
164
230
  if (!visitor_id) return;
165
- const dto = [{
231
+ const session_id = getSession().extend();
232
+ if (!session_id) return;
233
+ if (beacon([{
166
234
  name,
167
235
  properties,
168
236
  tags: cache.tags ?? {},
169
237
  visitor_id,
170
- session_id: getSession().extend(),
238
+ session_id,
171
239
  platform: config.platform,
172
240
  environment: config.environment,
173
241
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
174
- }];
175
- const blob = new Blob([JSON.stringify(dto)], { type: "application/json" });
176
- if (navigator.sendBeacon(`${config.endpoint}/events`, blob)) return;
242
+ }])) return;
177
243
  console.warn("Failed to send beacon", name, properties);
178
244
  }
179
245
  //#endregion
180
- export { sendBeacon, track, trackAsync };
246
+ export { sendBeacon, sendPendingEvents, track, trackAsync };
181
247
 
182
248
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"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.mjs","names":[],"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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"}
@@ -124,15 +124,18 @@ export interface PageInfo {
124
124
  */
125
125
  source_url?: string;
126
126
  }
127
+ /**
128
+ * Ad click ids and the ad platforms' first-party cookies, each under the name of where it was
129
+ * read: a URL parameter keeps the parameter's own name (`fbclid`, `gclid`, `ScCid` — jsonb keys
130
+ * are case-sensitive, so in SQL that is `tags->>'ScCid'`), a cookie keeps the cookie's name and
131
+ * so starts with an underscore (`_fbc`, `_gcl_aw`, `__oppref`). One source per key, never a
132
+ * fallback from one to the other, so a value always says where it came from.
133
+ *
134
+ * The URL parameters are this page's click and name the visit's channel (`CLICK_ID_CHANNELS`);
135
+ * the cookies outlive the click and only ever feed the conversion senders, which take the URL
136
+ * parameter first and the cookie when the page carries none (`server/click-ids`).
137
+ */
127
138
  export interface AdvertisingInfo {
128
- /**
129
- * Meta pixel fields
130
- * ref: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters#fbc
131
- * Stored in the _fbc/_fbp browser cookie under your domain
132
- * ref: https://www.facebook.com/business/help/2360940870872492?checkpoint_src=any
133
- */
134
- fbc?: string;
135
- fbp?: string;
136
139
  fbclid?: string;
137
140
  ad_id?: string;
138
141
  ad_name?: string;
@@ -141,26 +144,55 @@ export interface AdvertisingInfo {
141
144
  campaign_id?: string;
142
145
  campaign_name?: string;
143
146
  placement?: string;
144
- /** Google Analytics fields */
147
+ /**
148
+ * Meta click id cookie, `fb.<subdomainIndex>.<creationTime>.<fbclid>`, set server-side by
149
+ * `resolveClickIdCookies`.
150
+ * ref: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc
151
+ */
152
+ _fbc?: string;
153
+ /** Meta browser id cookie, `fb.<subdomainIndex>.<creationTime>.<random>`, set by the pixel. */
154
+ _fbp?: string;
155
+ /** @deprecated Renamed to `_fbc` in 9.0.0; still read from clients on an older version. */
156
+ fbc?: string;
157
+ /** @deprecated Renamed to `_fbp` in 9.0.0; still read from clients on an older version. */
158
+ fbp?: string;
145
159
  gclid?: string;
146
160
  gclsrc?: string;
147
161
  gad_source?: string;
148
162
  gad_campaignid?: string;
149
- /** Reddit Ads fields */
163
+ wbraid?: string;
164
+ gbraid?: string;
165
+ dclid?: string;
166
+ /** gtag's click cookie, `GCL.<seconds>.<gclid>`; see `parseGcl`. */
167
+ _gcl_aw?: string;
168
+ /** gtag's web-to-app click cookie, `GCL.<seconds>.<wbraid>`; see `parseGcl`. */
169
+ _gcl_gb?: string;
170
+ msclkid?: string;
171
+ /** The UET tag's click cookie, `_uet<msclkid>`; see `parseUetMsclkid`. */
172
+ _uetmsclkid?: string;
150
173
  rdt_cid?: string;
174
+ /** Reddit click id cookie, set server-side by `resolveClickIdCookies`. */
175
+ _rdt_cid?: string;
176
+ /** The Reddit pixel's browser id cookie, `<timestamp>.<uuid>`. */
177
+ _rdt_uuid?: string;
178
+ /** @deprecated Renamed to `_rdt_uuid` in 9.0.0; still read from clients on an older version. */
151
179
  rdt_uuid?: string;
152
- /** LinkedIn Ads fields: get click id from url params or first-party cookie */
153
180
  li_fat_id?: string;
154
- dclid?: string;
181
+ /**
182
+ * The Insight Tag's click cookie. The cookie itself is named `li_fat_id`, like the URL
183
+ * parameter; the underscore is ours, to keep the two apart.
184
+ */
185
+ _li_fat_id?: string;
186
+ oppref?: string;
187
+ /** The pixel's copy of the last `oppref`, kept 30 days. */
188
+ __oppref?: string;
189
+ /** The pixel's browser reference, a random id kept 365 days. */
190
+ __obref?: string;
155
191
  ko_click_id?: string;
156
- msclkid?: string;
157
192
  ScCid?: string;
158
193
  ttclid?: string;
159
194
  twclid?: string;
160
- wbraid?: string;
161
- gbraid?: string;
162
195
  yclid?: string;
163
- oppref?: string;
164
196
  epik?: string;
165
197
  }
166
198
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.cts","names":["GAUserProvidedData"],"sources":["../../src/track/types.ts"],"mappings":";;YAEY;YACA,YAAY;YAEZ,UAAU,UAAU,YAAY,aAAa,gBAAgB,iBACrE,IACA;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA+Ba;;;;;;;KAQZ,oBAAoB,KAAK,gBAAgB,wBAC1C,sBAAsB;YAGd,gBAAgB,UAAU,YAAY,aAAa,gBAAgB,iBAC3E,eAAe,KAAK,oBAAoB,KACxC,gBAAgB,wBACd,sBAAsB,KACtB,OAAO,mBAAmB;YAEpB;YACA;iBAEK;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;iBAGe,yBAAyBA;EACxC;EACA;EACA;EACA;EACA;IAAa;IAAc;IAAe;;EAE1C;EACA;;YAGU,qBAAqB,UAAU,WACzC,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B;iBAGe;EACf;EACA;EACA;EACA;EACA;EACA;;iBAGe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;;iBAGe;;EAEf;EACA;;iBAGe;EACf;EACA;EACA;;;;;;;;;;iBAWe;EACf;EACA;EACA;;;;;;;EAOA;;;;;;EAMA;;iBAGe;;;;;;;EAOf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;EACA;EACA;EACA;;EAEA;EACA;;EAEA;EAEA;EACA;EACA;EAGA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;;iBAQe;;EAEf;;;;;;;;;;;;EAqCA;;;;;;EAuDA;;EAMA;;EAEA;;EAEA;;EAEA;;EAUA;;EAEA;;iBAGe,kBACP,cAAc,YAAY,SAAS,iBAAiB,UAAU,iBAAiB;EACvF;GACC;;iBAGc,WAAW,UAAU,YAAY;EAChD;EACA,MAAM,UAAU;EAChB,MAAM;EACN;EACA;EACA,UAAU;EACV,aAAa;EACb,aAAa,gBAAgB;EAC7B;;YAGU;;EAEV"}
1
+ {"version":3,"file":"types.d.cts","names":["GAUserProvidedData"],"sources":["../../src/track/types.ts"],"mappings":";;YAEY;YACA,YAAY;YAEZ,UAAU,UAAU,YAAY,aAAa,gBAAgB,iBACrE,IACA;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA+Ba;;;;;;;KAQZ,oBAAoB,KAAK,gBAAgB,wBAC1C,sBAAsB;YAGd,gBAAgB,UAAU,YAAY,aAAa,gBAAgB,iBAC3E,eAAe,KAAK,oBAAoB,KACxC,gBAAgB,wBACd,sBAAsB,KACtB,OAAO,mBAAmB;YAEpB;YACA;iBAEK;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;iBAGe,yBAAyBA;EACxC;EACA;EACA;EACA;EACA;IAAa;IAAc;IAAe;;EAE1C;EACA;;YAGU,qBAAqB,UAAU,WACzC,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B;iBAGe;EACf;EACA;EACA;EACA;EACA;EACA;;iBAGe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;;iBAGe;;EAEf;EACA;;iBAGe;EACf;EACA;EACA;;;;;;;;;;iBAWe;EACf;EACA;EACA;;;;;;;EAOA;;;;;;EAMA;;;;;;;;;;;;;iBAce;EAEf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;EAMA;;EAEA;;EAEA;;EAEA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;;EAEA;EAEA;;EAEA;EAEA;;EAEA;;EAEA;;EAEA;EAEA;;;;;EAKA;EAEA;;EAEA;;EAEA;EAEA;EACA;EACA;EACA;EACA;EACA;;;;;;;iBAQe;;EAEf;;;;;;;;;;;;EAqCA;;;;;;EAuDA;;EAMA;;EAEA;;EAEA;;EAEA;;EAUA;;EAEA;;iBAGe,kBACP,cAAc,YAAY,SAAS,iBAAiB,UAAU,iBAAiB;EACvF;GACC;;iBAGc,WAAW,UAAU,YAAY;EAChD;EACA,MAAM,UAAU;EAChB,MAAM;EACN;EACA;EACA,UAAU;EACV,aAAa;EACb,aAAa,gBAAgB;EAC7B;;YAGU;;EAEV"}
@@ -124,15 +124,18 @@ export interface PageInfo {
124
124
  */
125
125
  source_url?: string;
126
126
  }
127
+ /**
128
+ * Ad click ids and the ad platforms' first-party cookies, each under the name of where it was
129
+ * read: a URL parameter keeps the parameter's own name (`fbclid`, `gclid`, `ScCid` — jsonb keys
130
+ * are case-sensitive, so in SQL that is `tags->>'ScCid'`), a cookie keeps the cookie's name and
131
+ * so starts with an underscore (`_fbc`, `_gcl_aw`, `__oppref`). One source per key, never a
132
+ * fallback from one to the other, so a value always says where it came from.
133
+ *
134
+ * The URL parameters are this page's click and name the visit's channel (`CLICK_ID_CHANNELS`);
135
+ * the cookies outlive the click and only ever feed the conversion senders, which take the URL
136
+ * parameter first and the cookie when the page carries none (`server/click-ids`).
137
+ */
127
138
  export interface AdvertisingInfo {
128
- /**
129
- * Meta pixel fields
130
- * ref: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters#fbc
131
- * Stored in the _fbc/_fbp browser cookie under your domain
132
- * ref: https://www.facebook.com/business/help/2360940870872492?checkpoint_src=any
133
- */
134
- fbc?: string;
135
- fbp?: string;
136
139
  fbclid?: string;
137
140
  ad_id?: string;
138
141
  ad_name?: string;
@@ -141,26 +144,55 @@ export interface AdvertisingInfo {
141
144
  campaign_id?: string;
142
145
  campaign_name?: string;
143
146
  placement?: string;
144
- /** Google Analytics fields */
147
+ /**
148
+ * Meta click id cookie, `fb.<subdomainIndex>.<creationTime>.<fbclid>`, set server-side by
149
+ * `resolveClickIdCookies`.
150
+ * ref: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc
151
+ */
152
+ _fbc?: string;
153
+ /** Meta browser id cookie, `fb.<subdomainIndex>.<creationTime>.<random>`, set by the pixel. */
154
+ _fbp?: string;
155
+ /** @deprecated Renamed to `_fbc` in 9.0.0; still read from clients on an older version. */
156
+ fbc?: string;
157
+ /** @deprecated Renamed to `_fbp` in 9.0.0; still read from clients on an older version. */
158
+ fbp?: string;
145
159
  gclid?: string;
146
160
  gclsrc?: string;
147
161
  gad_source?: string;
148
162
  gad_campaignid?: string;
149
- /** Reddit Ads fields */
163
+ wbraid?: string;
164
+ gbraid?: string;
165
+ dclid?: string;
166
+ /** gtag's click cookie, `GCL.<seconds>.<gclid>`; see `parseGcl`. */
167
+ _gcl_aw?: string;
168
+ /** gtag's web-to-app click cookie, `GCL.<seconds>.<wbraid>`; see `parseGcl`. */
169
+ _gcl_gb?: string;
170
+ msclkid?: string;
171
+ /** The UET tag's click cookie, `_uet<msclkid>`; see `parseUetMsclkid`. */
172
+ _uetmsclkid?: string;
150
173
  rdt_cid?: string;
174
+ /** Reddit click id cookie, set server-side by `resolveClickIdCookies`. */
175
+ _rdt_cid?: string;
176
+ /** The Reddit pixel's browser id cookie, `<timestamp>.<uuid>`. */
177
+ _rdt_uuid?: string;
178
+ /** @deprecated Renamed to `_rdt_uuid` in 9.0.0; still read from clients on an older version. */
151
179
  rdt_uuid?: string;
152
- /** LinkedIn Ads fields: get click id from url params or first-party cookie */
153
180
  li_fat_id?: string;
154
- dclid?: string;
181
+ /**
182
+ * The Insight Tag's click cookie. The cookie itself is named `li_fat_id`, like the URL
183
+ * parameter; the underscore is ours, to keep the two apart.
184
+ */
185
+ _li_fat_id?: string;
186
+ oppref?: string;
187
+ /** The pixel's copy of the last `oppref`, kept 30 days. */
188
+ __oppref?: string;
189
+ /** The pixel's browser reference, a random id kept 365 days. */
190
+ __obref?: string;
155
191
  ko_click_id?: string;
156
- msclkid?: string;
157
192
  ScCid?: string;
158
193
  ttclid?: string;
159
194
  twclid?: string;
160
- wbraid?: string;
161
- gbraid?: string;
162
195
  yclid?: string;
163
- oppref?: string;
164
196
  epik?: string;
165
197
  }
166
198
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.mts","names":["GAUserProvidedData"],"sources":["../../src/track/types.ts"],"mappings":";;YAEY;YACA,YAAY;YAEZ,UAAU,UAAU,YAAY,aAAa,gBAAgB,iBACrE,IACA;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA+Ba;;;;;;;KAQZ,oBAAoB,KAAK,gBAAgB,wBAC1C,sBAAsB;YAGd,gBAAgB,UAAU,YAAY,aAAa,gBAAgB,iBAC3E,eAAe,KAAK,oBAAoB,KACxC,gBAAgB,wBACd,sBAAsB,KACtB,OAAO,mBAAmB;YAEpB;YACA;iBAEK;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;iBAGe,yBAAyBA;EACxC;EACA;EACA;EACA;EACA;IAAa;IAAc;IAAe;;EAE1C;EACA;;YAGU,qBAAqB,UAAU,WACzC,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B;iBAGe;EACf;EACA;EACA;EACA;EACA;EACA;;iBAGe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;;iBAGe;;EAEf;EACA;;iBAGe;EACf;EACA;EACA;;;;;;;;;;iBAWe;EACf;EACA;EACA;;;;;;;EAOA;;;;;;EAMA;;iBAGe;;;;;;;EAOf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;EACA;EACA;EACA;;EAEA;EACA;;EAEA;EAEA;EACA;EACA;EAGA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;;iBAQe;;EAEf;;;;;;;;;;;;EAqCA;;;;;;EAuDA;;EAMA;;EAEA;;EAEA;;EAEA;;EAUA;;EAEA;;iBAGe,kBACP,cAAc,YAAY,SAAS,iBAAiB,UAAU,iBAAiB;EACvF;GACC;;iBAGc,WAAW,UAAU,YAAY;EAChD;EACA,MAAM,UAAU;EAChB,MAAM;EACN;EACA;EACA,UAAU;EACV,aAAa;EACb,aAAa,gBAAgB;EAC7B;;YAGU;;EAEV"}
1
+ {"version":3,"file":"types.d.mts","names":["GAUserProvidedData"],"sources":["../../src/track/types.ts"],"mappings":";;YAEY;YACA,YAAY;YAEZ,UAAU,UAAU,YAAY,aAAa,gBAAgB,iBACrE,IACA;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA+Ba;;;;;;;KAQZ,oBAAoB,KAAK,gBAAgB,wBAC1C,sBAAsB;YAGd,gBAAgB,UAAU,YAAY,aAAa,gBAAgB,iBAC3E,eAAe,KAAK,oBAAoB,KACxC,gBAAgB,wBACd,sBAAsB,KACtB,OAAO,mBAAmB;YAEpB;YACA;iBAEK;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;iBAGe,yBAAyBA;EACxC;EACA;EACA;EACA;EACA;IAAa;IAAc;IAAe;;EAE1C;EACA;;YAGU,qBAAqB,UAAU,WACzC,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B;iBAGe;EACf;EACA;EACA;EACA;EACA;EACA;;iBAGe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;;iBAGe;;EAEf;EACA;;iBAGe;EACf;EACA;EACA;;;;;;;;;;iBAWe;EACf;EACA;EACA;;;;;;;EAOA;;;;;;EAMA;;;;;;;;;;;;;iBAce;EAEf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;EAMA;;EAEA;;EAEA;;EAEA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;;EAEA;EAEA;;EAEA;EAEA;;EAEA;;EAEA;;EAEA;EAEA;;;;;EAKA;EAEA;;EAEA;;EAEA;EAEA;EACA;EACA;EACA;EACA;EACA;;;;;;;iBAQe;;EAEf;;;;;;;;;;;;EAqCA;;;;;;EAuDA;;EAMA;;EAEA;;EAEA;;EAEA;;EAUA;;EAEA;;iBAGe,kBACP,cAAc,YAAY,SAAS,iBAAiB,UAAU,iBAAiB;EACvF;GACC;;iBAGc,WAAW,UAAU,YAAY;EAChD;EACA,MAAM,UAAU;EAChB,MAAM;EACN;EACA;EACA,UAAU;EACV,aAAa;EACb,aAAa,gBAAgB;EAC7B;;YAGU;;EAEV"}