tracklane 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -5
- package/dist/browser-FeV5oRFZ.d.ts +628 -0
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +262 -6
- package/dist/browser.js.map +1 -1
- package/dist/index.d.ts +1 -3
- package/dist/server.d.ts +2 -2
- package/dist/server.js +289 -1
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
- package/dist/browser-usMUxJhU.d.ts +0 -126
- package/dist/server-DRda44Tb.d.ts +0 -166
- package/dist/vocabulary-fzM9ZuE1.d.ts +0 -129
package/dist/browser.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as createTracking, i as TrackOptions, n as BrowserTracking, o as
|
|
2
|
-
export { BrowserProvider, BrowserTracking, BrowserTrackingOptions, type Ga4BrowserConfig, TrackOptions, createTracking, ga4 };
|
|
1
|
+
import { a as createTracking, c as MetaBrowserConfig, d as ga4, i as TrackOptions, l as meta, n as BrowserTracking, o as PosthogBrowserConfig, r as BrowserTrackingOptions, s as posthog, t as BrowserProvider, u as Ga4BrowserConfig } from "./browser-FeV5oRFZ.js";
|
|
2
|
+
export { BrowserProvider, BrowserTracking, BrowserTrackingOptions, type Ga4BrowserConfig, type MetaBrowserConfig, type PosthogBrowserConfig, TrackOptions, createTracking, ga4, meta, posthog };
|
package/dist/browser.js
CHANGED
|
@@ -10,7 +10,7 @@ function gtagWindow() {
|
|
|
10
10
|
* library replicates that rather than swallowing it: a silent no-op is how a
|
|
11
11
|
* host ends up believing it is measuring something.
|
|
12
12
|
*/
|
|
13
|
-
function push(...args) {
|
|
13
|
+
function push$1(...args) {
|
|
14
14
|
const gtag = gtagWindow()?.gtag;
|
|
15
15
|
if (!gtag) throw new Error("ga4: window.gtag is not defined. Add Google's tag to the page before creating tracking.");
|
|
16
16
|
gtag(...args);
|
|
@@ -51,17 +51,273 @@ function ga4(measurementId, config = {}) {
|
|
|
51
51
|
default: "passthrough",
|
|
52
52
|
...events ? { events } : {},
|
|
53
53
|
track(name, data, _options) {
|
|
54
|
-
push("event", name, {
|
|
54
|
+
push$1("event", name, {
|
|
55
55
|
...data,
|
|
56
56
|
send_to: measurementId
|
|
57
57
|
});
|
|
58
58
|
},
|
|
59
59
|
identify(user, traits) {
|
|
60
|
-
if (user.userId !== void 0) push("set", { user_id: user.userId });
|
|
61
|
-
if (traits) push("set", "user_properties", traits);
|
|
60
|
+
if (user.userId !== void 0) push$1("set", { user_id: user.userId });
|
|
61
|
+
if (traits) push$1("set", "user_properties", traits);
|
|
62
62
|
},
|
|
63
63
|
consent(command, state) {
|
|
64
|
-
push("consent", command, state);
|
|
64
|
+
push$1("consent", command, state);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/providers/meta.browser.ts
|
|
70
|
+
/**
|
|
71
|
+
* The names Meta accepts through `fbq('track')`. Anything else is a custom
|
|
72
|
+
* event and goes through `trackCustom`, which is a different command rather
|
|
73
|
+
* than a different argument.
|
|
74
|
+
*
|
|
75
|
+
* Seventeen standard events plus `PageView`, which Meta documents separately
|
|
76
|
+
* from the seventeen and which the base snippet fires by itself.
|
|
77
|
+
*/
|
|
78
|
+
const STANDARD_EVENTS = [
|
|
79
|
+
"AddPaymentInfo",
|
|
80
|
+
"AddToCart",
|
|
81
|
+
"AddToWishlist",
|
|
82
|
+
"CompleteRegistration",
|
|
83
|
+
"Contact",
|
|
84
|
+
"CustomizeProduct",
|
|
85
|
+
"Donate",
|
|
86
|
+
"FindLocation",
|
|
87
|
+
"InitiateCheckout",
|
|
88
|
+
"Lead",
|
|
89
|
+
"PageView",
|
|
90
|
+
"Purchase",
|
|
91
|
+
"Schedule",
|
|
92
|
+
"Search",
|
|
93
|
+
"StartTrial",
|
|
94
|
+
"SubmitApplication",
|
|
95
|
+
"Subscribe",
|
|
96
|
+
"ViewContent"
|
|
97
|
+
];
|
|
98
|
+
/**
|
|
99
|
+
* How Meta spells the canonical vocabulary, which is GA4's.
|
|
100
|
+
*
|
|
101
|
+
* Only canonical names appear here. Meta has eight more standard events with
|
|
102
|
+
* no GA4 counterpart (`Subscribe`, `StartTrial`, `Schedule`, `Contact`,
|
|
103
|
+
* `Donate`, `FindLocation`, `CustomizeProduct`, `SubmitApplication`), and
|
|
104
|
+
* inventing canonical names for them would make this library the author of a
|
|
105
|
+
* vocabulary it deliberately borrows. A host that fires those maps its own
|
|
106
|
+
* event name to them through `events`.
|
|
107
|
+
*
|
|
108
|
+
* A canonical event with no standard counterpart is **not** dropped: it goes
|
|
109
|
+
* to `trackCustom` under its own name, because by hand that event would have
|
|
110
|
+
* been sent.
|
|
111
|
+
*/
|
|
112
|
+
const CANONICAL_EVENTS = {
|
|
113
|
+
page_view: "PageView",
|
|
114
|
+
view_item: "ViewContent",
|
|
115
|
+
search: "Search",
|
|
116
|
+
add_to_cart: "AddToCart",
|
|
117
|
+
add_to_wishlist: "AddToWishlist",
|
|
118
|
+
begin_checkout: "InitiateCheckout",
|
|
119
|
+
add_payment_info: "AddPaymentInfo",
|
|
120
|
+
purchase: "Purchase",
|
|
121
|
+
sign_up: "CompleteRegistration",
|
|
122
|
+
generate_lead: "Lead"
|
|
123
|
+
};
|
|
124
|
+
function fbqWindow() {
|
|
125
|
+
return typeof window === "undefined" ? void 0 : window;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Sends a command, or fails the way a hand-written call would.
|
|
129
|
+
*
|
|
130
|
+
* Writing `fbq(…)` on a page without Meta's snippet throws, and this library
|
|
131
|
+
* replicates that rather than swallowing it: a silent no-op is how a host
|
|
132
|
+
* ends up believing it is measuring something.
|
|
133
|
+
*/
|
|
134
|
+
function push(...args) {
|
|
135
|
+
const fbq = fbqWindow()?.fbq;
|
|
136
|
+
if (!fbq) throw new Error("meta: window.fbq is not defined. Add Meta's pixel to the page before creating tracking.");
|
|
137
|
+
fbq(...args);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* GA4's `items` become Meta's `contents`, which is a closed shape: `id`,
|
|
141
|
+
* `quantity`, `item_price` and `delivery_category`, and nothing else.
|
|
142
|
+
*
|
|
143
|
+
* The rest of a GA4 item, `item_name` and `item_brand`, is dropped rather than
|
|
144
|
+
* forwarded, because a key Meta does not read inside `contents` looks mapped
|
|
145
|
+
* and arrives nowhere. That is the failure mode this project keeps finding.
|
|
146
|
+
*/
|
|
147
|
+
function contents(items) {
|
|
148
|
+
return items.map((item) => {
|
|
149
|
+
const content = {};
|
|
150
|
+
if (item.item_id !== void 0) content.id = item.item_id;
|
|
151
|
+
if (item.quantity !== void 0) content.quantity = item.quantity;
|
|
152
|
+
if (item.price !== void 0) content.item_price = item.price;
|
|
153
|
+
return content;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* `value` and `currency` are spelled identically by both vendors, so they
|
|
158
|
+
* pass through untouched. Everything the host named itself passes through
|
|
159
|
+
* too: Meta keeps unknown keys as custom properties, which is exactly what a
|
|
160
|
+
* hand-written call would have produced.
|
|
161
|
+
*
|
|
162
|
+
* `transaction_id` stays under its own name here. Meta's pixel documents no
|
|
163
|
+
* order id among its object properties. `order_id` exists on the Conversions
|
|
164
|
+
* API and not on this surface, so translating it would be inventing a slot.
|
|
165
|
+
*/
|
|
166
|
+
function pixelParams(data) {
|
|
167
|
+
const { items, ...rest } = data;
|
|
168
|
+
const params = { ...rest };
|
|
169
|
+
if (items) params.contents = contents(items);
|
|
170
|
+
return params;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Meta's pixel has one consent switch where this library carries Google's
|
|
174
|
+
* four signals, so the four are collapsed into it.
|
|
175
|
+
*
|
|
176
|
+
* The collapse is the strictest declared answer: any `denied` produces
|
|
177
|
+
* `revoke`, and `grant` needs every declared signal to be `granted`. There is
|
|
178
|
+
* no field to partition, so this is the only reduction that cannot widen a
|
|
179
|
+
* denial, and widening is the one thing a projection must never do.
|
|
180
|
+
*
|
|
181
|
+
* A host that wants Meta to follow only its advertising category, and not its
|
|
182
|
+
* analytics one, decides that before `createTracking` by leaving this
|
|
183
|
+
* provider out, which is what `@tracklane/consent` exists to do.
|
|
184
|
+
*/
|
|
185
|
+
function collapse(state) {
|
|
186
|
+
const signals = [
|
|
187
|
+
state.ad_storage,
|
|
188
|
+
state.analytics_storage,
|
|
189
|
+
state.ad_user_data,
|
|
190
|
+
state.ad_personalization
|
|
191
|
+
].filter((signal) => signal !== void 0);
|
|
192
|
+
if (signals.length === 0) return void 0;
|
|
193
|
+
return signals.every((signal) => signal === "granted") ? "grant" : "revoke";
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* The Meta pixel in the browser, through `fbq`.
|
|
197
|
+
*
|
|
198
|
+
* **This provider does not load Meta's pixel.** The base code belongs on the
|
|
199
|
+
* page, installed the way
|
|
200
|
+
* [Meta documents it](https://developers.facebook.com/docs/meta-pixel/get-started):
|
|
201
|
+
* a snippet in the HTML, or a tag manager. This library talks to the pixel
|
|
202
|
+
* that is already there; it never injects a third-party script and never
|
|
203
|
+
* issues `fbq('init')`.
|
|
204
|
+
*
|
|
205
|
+
* Three consequences follow from that, and all three are Meta's rules rather
|
|
206
|
+
* than ours:
|
|
207
|
+
*
|
|
208
|
+
* - **There is no `identify` here.** Meta accepts advanced matching only in
|
|
209
|
+
* the base code ("be sure to place advanced matching parameters in the
|
|
210
|
+
* pixel base code or the values will not be treated as manual advanced
|
|
211
|
+
* matching values"), and the base code is yours. Put `em`, `ph`, `fn`, `ln`
|
|
212
|
+
* in your own `fbq('init')`, where the pixel normalises and hashes them for
|
|
213
|
+
* you. On the server, where there is no snippet, the same identity travels
|
|
214
|
+
* in `context.user` and is hashed by the adapter.
|
|
215
|
+
* - **The initial consent declaration is not ours.** Meta wants
|
|
216
|
+
* `fbq('consent', 'revoke')` before `init`, and by the time you hold what
|
|
217
|
+
* `createTracking` returns that moment has passed. Ours is every
|
|
218
|
+
* declaration after it, through `consent()`.
|
|
219
|
+
* - **Limited Data Use is not ours either.** `fbq('dataProcessingOptions', …)`
|
|
220
|
+
* is documented as running before `init`. It belongs in your snippet. The
|
|
221
|
+
* server half, which has no snippet, carries it on its factory.
|
|
222
|
+
*
|
|
223
|
+
* **There is no pixel id here**, and that is deliberate rather than an
|
|
224
|
+
* omission. `fbq('track')` reaches every pixel the page initialised, so an id
|
|
225
|
+
* passed in would address nothing and the parameter would promise something
|
|
226
|
+
* this adapter cannot deliver. Which pixels exist is declared by your snippet,
|
|
227
|
+
* in `fbq('init')`.
|
|
228
|
+
*
|
|
229
|
+
* @param config - Event name overrides, merged over the built-in map.
|
|
230
|
+
* @returns A provider for `createTracking` from `tracklane/browser`.
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* ```ts
|
|
234
|
+
* import { createTracking, meta } from 'tracklane/browser';
|
|
235
|
+
*
|
|
236
|
+
* // Meta's own base code is already in the page, and it named the pixel.
|
|
237
|
+
* const { track } = createTracking({ providers: [meta()] });
|
|
238
|
+
*
|
|
239
|
+
* // fbq('track', 'Purchase', { value: 49.9, currency: 'BRL' }, { eventID: 'T-1' })
|
|
240
|
+
* track('purchase', { value: 49.9, currency: 'BRL' }, { dedupId: 'T-1' });
|
|
241
|
+
* ```
|
|
242
|
+
*/
|
|
243
|
+
function meta(config = {}) {
|
|
244
|
+
return {
|
|
245
|
+
name: "meta",
|
|
246
|
+
default: "passthrough",
|
|
247
|
+
events: {
|
|
248
|
+
...CANONICAL_EVENTS,
|
|
249
|
+
...config.events
|
|
250
|
+
},
|
|
251
|
+
track(name, data, options) {
|
|
252
|
+
const args = [
|
|
253
|
+
STANDARD_EVENTS.includes(name) ? "track" : "trackCustom",
|
|
254
|
+
name,
|
|
255
|
+
pixelParams(data)
|
|
256
|
+
];
|
|
257
|
+
if (options.dedupId !== void 0) args.push({ eventID: options.dedupId });
|
|
258
|
+
push(...args);
|
|
259
|
+
},
|
|
260
|
+
consent(_command, state) {
|
|
261
|
+
const answer = collapse(state);
|
|
262
|
+
if (answer) push("consent", answer);
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
//#endregion
|
|
267
|
+
//#region src/providers/posthog.browser.ts
|
|
268
|
+
function posthogWindow() {
|
|
269
|
+
return typeof window === "undefined" ? void 0 : window;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Returns the vendor SDK, or fails the way a hand-written call would.
|
|
273
|
+
*
|
|
274
|
+
* Calling `posthog.capture(...)` on a page without PostHog's snippet throws,
|
|
275
|
+
* and this library replicates that rather than swallowing it: a silent no-op
|
|
276
|
+
* is how a host ends up believing it is measuring something.
|
|
277
|
+
*/
|
|
278
|
+
function sdk() {
|
|
279
|
+
const instance = posthogWindow()?.posthog;
|
|
280
|
+
if (!instance) throw new Error("posthog: window.posthog is not defined. Add PostHog's snippet to the page before creating tracking.");
|
|
281
|
+
return instance;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* PostHog in the browser, through `posthog-js`.
|
|
285
|
+
*
|
|
286
|
+
* **This provider does not load PostHog's snippet.** It belongs on the page,
|
|
287
|
+
* installed the way PostHog documents it: a snippet in the HTML, or a tag
|
|
288
|
+
* manager. This library talks to the instance that is already there; it
|
|
289
|
+
* never injects a third-party script into your page.
|
|
290
|
+
*
|
|
291
|
+
* @param config - Rarely needed event name overrides.
|
|
292
|
+
* @returns A provider for `createTracking` from `tracklane/browser`.
|
|
293
|
+
*
|
|
294
|
+
* @example
|
|
295
|
+
* ```ts
|
|
296
|
+
* import { createTracking, posthog } from 'tracklane/browser';
|
|
297
|
+
*
|
|
298
|
+
* // PostHog's own snippet is already in the page.
|
|
299
|
+
* const { track } = createTracking({ providers: [posthog()] });
|
|
300
|
+
*
|
|
301
|
+
* track('purchase', { transaction_id: 'T-1', value: 49.9, currency: 'BRL' });
|
|
302
|
+
* ```
|
|
303
|
+
*/
|
|
304
|
+
function posthog(config = {}) {
|
|
305
|
+
const { events } = config;
|
|
306
|
+
return {
|
|
307
|
+
name: "posthog",
|
|
308
|
+
default: "passthrough",
|
|
309
|
+
...events ? { events } : {},
|
|
310
|
+
track(name, data, _options) {
|
|
311
|
+
sdk().capture(name, data);
|
|
312
|
+
},
|
|
313
|
+
identify(user, traits) {
|
|
314
|
+
if (user.userId !== void 0) sdk().identify(user.userId);
|
|
315
|
+
if (traits) sdk().setPersonProperties(traits);
|
|
316
|
+
},
|
|
317
|
+
consent(_command, state) {
|
|
318
|
+
const { analytics_storage } = state;
|
|
319
|
+
if (analytics_storage === "denied") sdk().opt_out_capturing();
|
|
320
|
+
else if (analytics_storage === "granted") sdk().opt_in_capturing();
|
|
65
321
|
}
|
|
66
322
|
};
|
|
67
323
|
}
|
|
@@ -136,6 +392,6 @@ function createTracking(options) {
|
|
|
136
392
|
};
|
|
137
393
|
}
|
|
138
394
|
//#endregion
|
|
139
|
-
export { createTracking, ga4 };
|
|
395
|
+
export { createTracking, ga4, meta, posthog };
|
|
140
396
|
|
|
141
397
|
//# sourceMappingURL=browser.js.map
|
package/dist/browser.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser.js","names":[],"sources":["../src/providers/ga4.browser.ts","../src/browser.ts"],"sourcesContent":["import type { BrowserProvider, TrackOptions } from '../browser.js';\nimport type { ConsentState, EventBinding, EventData, UserData } from '../vocabulary.js';\n\ninterface GtagWindow {\n gtag?: (...args: unknown[]) => void;\n}\n\n/** Optional configuration for the GA4 browser provider. */\nexport interface Ga4BrowserConfig {\n /**\n * How this property spells each canonical event. Rarely needed: the\n * canonical vocabulary already is GA4's.\n */\n events?: Record<string, EventBinding<string>>;\n}\n\n// Server rendering reaches this module with no browser at all, where\n// `window` is a ReferenceError rather than `undefined`.\nfunction gtagWindow(): GtagWindow | undefined {\n return typeof window === 'undefined' ? undefined : (window as unknown as GtagWindow);\n}\n\n/**\n * Sends a command, or fails the way a hand-written call would.\n *\n * Writing `gtag(…)` on a page without Google's snippet throws, and this\n * library replicates that rather than swallowing it: a silent no-op is how a\n * host ends up believing it is measuring something.\n */\nfunction push(...args: unknown[]): void {\n const gtag = gtagWindow()?.gtag;\n\n if (!gtag) {\n throw new Error(\n \"ga4: window.gtag is not defined. Add Google's tag to the page before creating tracking.\",\n );\n }\n\n gtag(...args);\n}\n\n/**\n * Google Analytics 4 in the browser, through `gtag.js`.\n *\n * **This provider does not load Google's tag.** The tag belongs on the page,\n * installed the way Google documents it: a snippet in the HTML, or a tag\n * manager. This library talks to the tag that is already there; it never\n * injects a third-party script into your page, and it never issues the\n * property configuration that would emit a second page view.\n *\n * Which also means the initial consent declaration is not ours to make:\n * Google requires it before the property is configured, and the snippet that\n * configures the property is yours. Use {@link ConsentState} through\n * `consent()` for changes after that.\n *\n * @param measurementId - The `G-` id of the data stream, matching the tag on\n * the page.\n * @param config - Rarely needed event name overrides.\n * @returns A provider for `createTracking` from `tracklane/browser`.\n *\n * @example\n * ```ts\n * import { createTracking, ga4 } from 'tracklane/browser';\n *\n * // Google's own snippet is already in the page.\n * const { track } = createTracking({ providers: [ga4('G-XXXXXXX')] });\n *\n * track('purchase', { transaction_id: 'T-1', value: 49.9, currency: 'BRL' });\n * ```\n */\nexport function ga4(measurementId: string, config: Ga4BrowserConfig = {}): BrowserProvider {\n const { events } = config;\n\n return {\n name: 'ga4',\n // The canonical names already are GA4's, so every event passes through\n // under its own name unless the host says otherwise.\n default: 'passthrough',\n ...(events ? { events } : {}),\n\n track(name: string, data: EventData, _options: TrackOptions): void {\n // GA4 documents no browser-to-server deduplication, so `dedupId` maps\n // to nothing here. That is the exact translation of \"this concept does\n // not exist in this vendor\". Inventing a param would imply a merge\n // the platform never performs.\n //\n // `send_to` is written after the host's data on purpose: without it,\n // gtag sends to every configured property on the page, so the\n // addressing is the adapter's to own and not the host's to override.\n push('event', name, { ...data, send_to: measurementId });\n },\n\n identify(user: UserData, traits?: Record<string, unknown>): void {\n // GA4 documents no browser slot for email, phone or names. They map to\n // nothing, which is what \"this concept does not exist here\" means.\n if (user.userId !== undefined) push('set', { user_id: user.userId });\n if (traits) push('set', 'user_properties', traits);\n },\n\n consent(command: 'default' | 'update', state: ConsentState): void {\n // Verbatim: the library's consent vocabulary is Google's own spelling,\n // so there is nothing to translate and no chance to expand a signal\n // the host did not declare.\n push('consent', command, state);\n },\n };\n}\n","import { assertUniqueProviders, makeReporter, messageOf, resolveEvent } from './dispatch.js';\n\nexport { type Ga4BrowserConfig, ga4 } from './providers/ga4.browser.js';\n\nimport type {\n ConsentState,\n EventBinding,\n EventData,\n ProviderDefault,\n TrackingError,\n UserData,\n} from './vocabulary.js';\n\n/**\n * Everything the browser needs beyond the event itself.\n *\n * There is one field, and that is the point: in the browser each vendor tag\n * reads its own cookies and holds its own session, so the only thing left\n * for the host to supply is the value that ties this hit to a server-side\n * one.\n */\nexport interface TrackOptions {\n /**\n * The value that lets a vendor recognise this hit and a server-side hit as\n * one event. The library never invents it: the browser and the server do\n * not share a process, so only a value the host already owns (an order id)\n * can match on both sides.\n */\n dedupId?: string;\n}\n\n/**\n * A destination for browser events.\n *\n * This is the whole contract. A provider written outside this package\n * implements it exactly as the providers shipped here do.\n */\nexport interface BrowserProvider<Target = string> {\n /** The vendor's fixed name. Registering one twice is an error. */\n name: string;\n /** What happens to a canonical event this vendor has no binding for. */\n default: ProviderDefault;\n /** How this vendor spells each canonical event. */\n events?: Record<string, EventBinding<Target>>;\n /** Loads the vendor tag. Runs once, at creation. */\n install?(): void | Promise<void>;\n /** Sends one event to this vendor. Throwing here reaches `onError`, not the caller. */\n track(name: Target, data: EventData, options: TrackOptions): void;\n /** Absent where the vendor keeps no user of its own. */\n identify?(user: UserData, traits?: Record<string, unknown>): void;\n /** Absent where the vendor documents no consent command. */\n consent?(command: 'default' | 'update', state: ConsentState): void;\n}\n\n/** Options accepted by {@link createTracking}. */\nexport interface BrowserTrackingOptions {\n /** The vendors to send every event to. Registering the same `name` twice throws. */\n providers: readonly BrowserProvider[];\n /**\n * The one diagnostic channel. Without it the library is silent, exactly as\n * a hand-written vendor call is.\n */\n onError?: (error: TrackingError) => void;\n}\n\n/** What {@link createTracking} returns. */\nexport interface BrowserTracking {\n /** Sends one event to every configured vendor. Never throws. */\n track(name: string, data?: EventData, options?: TrackOptions): void;\n /** Forwards the user to the vendors that hold one, and to nobody else. */\n identify(user: UserData, traits?: Record<string, unknown>): void;\n /**\n * Forwards a consent declaration to every vendor that documents a command\n * for it. It never decides, stores or withholds anything.\n */\n consent(command: 'default' | 'update', state: ConsentState): void;\n}\n\n/**\n * Creates the browser tracking surface.\n *\n * Providers install eagerly, which is what pasting a vendor snippet does and\n * what lets each vendor record its own initial page view. Every vendor\n * shipped here queues its own commands until its script lands, so nothing is\n * lost to loading and this library needs no queue of its own.\n *\n * @param options - The providers to send to, and an optional error channel.\n * @returns The `track`, `identify` and `consent` calls.\n *\n * @example\n * ```ts\n * import { createTracking, ga4 } from 'tracklane/browser';\n *\n * const { track } = createTracking({ providers: [ga4('G-XXXXXXX')] });\n * track('purchase', { transaction_id: 'T-1', value: 49.9, currency: 'BRL' });\n * ```\n */\nexport function createTracking(options: BrowserTrackingOptions): BrowserTracking {\n const { providers } = options;\n assertUniqueProviders(providers);\n\n const report = makeReporter(options.onError);\n\n const fail = (provider: string, event: string, cause: unknown): void => {\n report({ provider, event, severity: 'error', message: messageOf(cause), cause });\n };\n\n for (const provider of providers) {\n try {\n const installing = provider.install?.();\n // An async loader that rejects would otherwise become an unhandled\n // rejection, the silent failure this channel exists to prevent.\n void Promise.resolve(installing).catch((cause: unknown) => {\n fail(provider.name, 'install', cause);\n });\n } catch (cause) {\n fail(provider.name, 'install', cause);\n }\n }\n\n return {\n track(name, data = {}, trackOptions = {}) {\n for (const provider of providers) {\n const target = resolveEvent(provider, name);\n if (target === undefined) continue;\n\n try {\n provider.track(target, data, trackOptions);\n } catch (cause) {\n fail(provider.name, name, cause);\n }\n }\n },\n\n identify(user, traits) {\n for (const provider of providers) {\n try {\n provider.identify?.(user, traits);\n } catch (cause) {\n fail(provider.name, 'identify', cause);\n }\n }\n },\n\n consent(command, state) {\n for (const provider of providers) {\n try {\n provider.consent?.(command, state);\n } catch (cause) {\n fail(provider.name, 'consent', cause);\n }\n }\n },\n };\n}\n"],"mappings":";;AAkBA,SAAS,aAAqC;CAC5C,OAAO,OAAO,WAAW,cAAc,KAAA,IAAa;AACtD;;;;;;;;AASA,SAAS,KAAK,GAAG,MAAuB;CACtC,MAAM,OAAO,WAAW,CAAC,EAAE;CAE3B,IAAI,CAAC,MACH,MAAM,IAAI,MACR,yFACF;CAGF,KAAK,GAAG,IAAI;AACd;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,SAAgB,IAAI,eAAuB,SAA2B,CAAC,GAAoB;CACzF,MAAM,EAAE,WAAW;CAEnB,OAAO;EACL,MAAM;EAGN,SAAS;EACT,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;EAE3B,MAAM,MAAc,MAAiB,UAA8B;GASjE,KAAK,SAAS,MAAM;IAAE,GAAG;IAAM,SAAS;GAAc,CAAC;EACzD;EAEA,SAAS,MAAgB,QAAwC;GAG/D,IAAI,KAAK,WAAW,KAAA,GAAW,KAAK,OAAO,EAAE,SAAS,KAAK,OAAO,CAAC;GACnE,IAAI,QAAQ,KAAK,OAAO,mBAAmB,MAAM;EACnD;EAEA,QAAQ,SAA+B,OAA2B;GAIhE,KAAK,WAAW,SAAS,KAAK;EAChC;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;ACTA,SAAgB,eAAe,SAAkD;CAC/E,MAAM,EAAE,cAAc;CACtB,sBAAsB,SAAS;CAE/B,MAAM,SAAS,aAAa,QAAQ,OAAO;CAE3C,MAAM,QAAQ,UAAkB,OAAe,UAAyB;EACtE,OAAO;GAAE;GAAU;GAAO,UAAU;GAAS,SAAS,UAAU,KAAK;GAAG;EAAM,CAAC;CACjF;CAEA,KAAK,MAAM,YAAY,WACrB,IAAI;EACF,MAAM,aAAa,SAAS,UAAU;EAGtC,QAAa,QAAQ,UAAU,CAAC,CAAC,OAAO,UAAmB;GACzD,KAAK,SAAS,MAAM,WAAW,KAAK;EACtC,CAAC;CACH,SAAS,OAAO;EACd,KAAK,SAAS,MAAM,WAAW,KAAK;CACtC;CAGF,OAAO;EACL,MAAM,MAAM,OAAO,CAAC,GAAG,eAAe,CAAC,GAAG;GACxC,KAAK,MAAM,YAAY,WAAW;IAChC,MAAM,SAAS,aAAa,UAAU,IAAI;IAC1C,IAAI,WAAW,KAAA,GAAW;IAE1B,IAAI;KACF,SAAS,MAAM,QAAQ,MAAM,YAAY;IAC3C,SAAS,OAAO;KACd,KAAK,SAAS,MAAM,MAAM,KAAK;IACjC;GACF;EACF;EAEA,SAAS,MAAM,QAAQ;GACrB,KAAK,MAAM,YAAY,WACrB,IAAI;IACF,SAAS,WAAW,MAAM,MAAM;GAClC,SAAS,OAAO;IACd,KAAK,SAAS,MAAM,YAAY,KAAK;GACvC;EAEJ;EAEA,QAAQ,SAAS,OAAO;GACtB,KAAK,MAAM,YAAY,WACrB,IAAI;IACF,SAAS,UAAU,SAAS,KAAK;GACnC,SAAS,OAAO;IACd,KAAK,SAAS,MAAM,WAAW,KAAK;GACtC;EAEJ;CACF;AACF"}
|
|
1
|
+
{"version":3,"file":"browser.js","names":["push"],"sources":["../src/providers/ga4.browser.ts","../src/providers/meta.browser.ts","../src/providers/posthog.browser.ts","../src/browser.ts"],"sourcesContent":["// Through the public entry point, exactly as a provider written elsewhere\n// would import it. A provider that needs something not exported here is\n// telling us the contract is incomplete, which is a conversation rather than\n// a reason to reach inside.\nimport type {\n BrowserProvider,\n ConsentState,\n EventBinding,\n EventData,\n TrackOptions,\n UserData,\n} from '../index.js';\n\ninterface GtagWindow {\n gtag?: (...args: unknown[]) => void;\n}\n\n/** Optional configuration for the GA4 browser provider. */\nexport interface Ga4BrowserConfig {\n /**\n * How this property spells each canonical event. Rarely needed: the\n * canonical vocabulary already is GA4's.\n */\n events?: Record<string, EventBinding<string>>;\n}\n\n// Server rendering reaches this module with no browser at all, where\n// `window` is a ReferenceError rather than `undefined`.\nfunction gtagWindow(): GtagWindow | undefined {\n return typeof window === 'undefined' ? undefined : (window as unknown as GtagWindow);\n}\n\n/**\n * Sends a command, or fails the way a hand-written call would.\n *\n * Writing `gtag(…)` on a page without Google's snippet throws, and this\n * library replicates that rather than swallowing it: a silent no-op is how a\n * host ends up believing it is measuring something.\n */\nfunction push(...args: unknown[]): void {\n const gtag = gtagWindow()?.gtag;\n\n if (!gtag) {\n throw new Error(\n \"ga4: window.gtag is not defined. Add Google's tag to the page before creating tracking.\",\n );\n }\n\n gtag(...args);\n}\n\n/**\n * Google Analytics 4 in the browser, through `gtag.js`.\n *\n * **This provider does not load Google's tag.** The tag belongs on the page,\n * installed the way Google documents it: a snippet in the HTML, or a tag\n * manager. This library talks to the tag that is already there; it never\n * injects a third-party script into your page, and it never issues the\n * property configuration that would emit a second page view.\n *\n * Which also means the initial consent declaration is not ours to make:\n * Google requires it before the property is configured, and the snippet that\n * configures the property is yours. Use {@link ConsentState} through\n * `consent()` for changes after that.\n *\n * @param measurementId - The `G-` id of the data stream, matching the tag on\n * the page.\n * @param config - Rarely needed event name overrides.\n * @returns A provider for `createTracking` from `tracklane/browser`.\n *\n * @example\n * ```ts\n * import { createTracking, ga4 } from 'tracklane/browser';\n *\n * // Google's own snippet is already in the page.\n * const { track } = createTracking({ providers: [ga4('G-XXXXXXX')] });\n *\n * track('purchase', { transaction_id: 'T-1', value: 49.9, currency: 'BRL' });\n * ```\n */\nexport function ga4(measurementId: string, config: Ga4BrowserConfig = {}): BrowserProvider {\n const { events } = config;\n\n return {\n name: 'ga4',\n // The canonical names already are GA4's, so every event passes through\n // under its own name unless the host says otherwise.\n default: 'passthrough',\n ...(events ? { events } : {}),\n\n track(name: string, data: EventData, _options: TrackOptions): void {\n // GA4 documents no browser-to-server deduplication, so `dedupId` maps\n // to nothing here. That is the exact translation of \"this concept does\n // not exist in this vendor\". Inventing a param would imply a merge\n // the platform never performs.\n //\n // `send_to` is written after the host's data on purpose: without it,\n // gtag sends to every configured property on the page, so the\n // addressing is the adapter's to own and not the host's to override.\n push('event', name, { ...data, send_to: measurementId });\n },\n\n identify(user: UserData, traits?: Record<string, unknown>): void {\n // GA4 documents no browser slot for email, phone or names. They map to\n // nothing, which is what \"this concept does not exist here\" means.\n if (user.userId !== undefined) push('set', { user_id: user.userId });\n if (traits) push('set', 'user_properties', traits);\n },\n\n consent(command: 'default' | 'update', state: ConsentState): void {\n // Verbatim: the library's consent vocabulary is Google's own spelling,\n // so there is nothing to translate and no chance to expand a signal\n // the host did not declare.\n push('consent', command, state);\n },\n };\n}\n","// Through the public entry point, exactly as a provider written elsewhere\n// would import it. A provider that needs something not exported here is\n// telling us the contract is incomplete, which is a conversation rather than\n// a reason to reach inside.\nimport type {\n BrowserProvider,\n ConsentState,\n EventBinding,\n EventData,\n EventItem,\n TrackOptions,\n} from '../index.js';\n\ninterface FbqWindow {\n fbq?: (...args: unknown[]) => void;\n}\n\n/**\n * The names Meta accepts through `fbq('track')`. Anything else is a custom\n * event and goes through `trackCustom`, which is a different command rather\n * than a different argument.\n *\n * Seventeen standard events plus `PageView`, which Meta documents separately\n * from the seventeen and which the base snippet fires by itself.\n */\nconst STANDARD_EVENTS: readonly string[] = [\n 'AddPaymentInfo',\n 'AddToCart',\n 'AddToWishlist',\n 'CompleteRegistration',\n 'Contact',\n 'CustomizeProduct',\n 'Donate',\n 'FindLocation',\n 'InitiateCheckout',\n 'Lead',\n 'PageView',\n 'Purchase',\n 'Schedule',\n 'Search',\n 'StartTrial',\n 'SubmitApplication',\n 'Subscribe',\n 'ViewContent',\n];\n\n/**\n * How Meta spells the canonical vocabulary, which is GA4's.\n *\n * Only canonical names appear here. Meta has eight more standard events with\n * no GA4 counterpart (`Subscribe`, `StartTrial`, `Schedule`, `Contact`,\n * `Donate`, `FindLocation`, `CustomizeProduct`, `SubmitApplication`), and\n * inventing canonical names for them would make this library the author of a\n * vocabulary it deliberately borrows. A host that fires those maps its own\n * event name to them through `events`.\n *\n * A canonical event with no standard counterpart is **not** dropped: it goes\n * to `trackCustom` under its own name, because by hand that event would have\n * been sent.\n */\nconst CANONICAL_EVENTS: Record<string, EventBinding<string>> = {\n page_view: 'PageView',\n view_item: 'ViewContent',\n search: 'Search',\n add_to_cart: 'AddToCart',\n add_to_wishlist: 'AddToWishlist',\n begin_checkout: 'InitiateCheckout',\n add_payment_info: 'AddPaymentInfo',\n purchase: 'Purchase',\n sign_up: 'CompleteRegistration',\n generate_lead: 'Lead',\n};\n\n/** Optional configuration for the Meta browser provider. */\nexport interface MetaBrowserConfig {\n /**\n * How this pixel spells each canonical event, merged over the built-in map.\n *\n * This is how the eight standard events with no GA4 counterpart are\n * reached, and how an event is kept out of Meta entirely with `null`.\n */\n events?: Record<string, EventBinding<string>>;\n}\n\n// Server rendering reaches this module with no browser at all, where\n// `window` is a ReferenceError rather than `undefined`.\nfunction fbqWindow(): FbqWindow | undefined {\n return typeof window === 'undefined' ? undefined : (window as unknown as FbqWindow);\n}\n\n/**\n * Sends a command, or fails the way a hand-written call would.\n *\n * Writing `fbq(…)` on a page without Meta's snippet throws, and this library\n * replicates that rather than swallowing it: a silent no-op is how a host\n * ends up believing it is measuring something.\n */\nfunction push(...args: unknown[]): void {\n const fbq = fbqWindow()?.fbq;\n\n if (!fbq) {\n throw new Error(\n \"meta: window.fbq is not defined. Add Meta's pixel to the page before creating tracking.\",\n );\n }\n\n fbq(...args);\n}\n\n/**\n * GA4's `items` become Meta's `contents`, which is a closed shape: `id`,\n * `quantity`, `item_price` and `delivery_category`, and nothing else.\n *\n * The rest of a GA4 item, `item_name` and `item_brand`, is dropped rather than\n * forwarded, because a key Meta does not read inside `contents` looks mapped\n * and arrives nowhere. That is the failure mode this project keeps finding.\n */\nfunction contents(items: readonly EventItem[]): Record<string, unknown>[] {\n return items.map((item) => {\n const content: Record<string, unknown> = {};\n\n if (item.item_id !== undefined) content.id = item.item_id;\n if (item.quantity !== undefined) content.quantity = item.quantity;\n if (item.price !== undefined) content.item_price = item.price;\n\n return content;\n });\n}\n\n/**\n * `value` and `currency` are spelled identically by both vendors, so they\n * pass through untouched. Everything the host named itself passes through\n * too: Meta keeps unknown keys as custom properties, which is exactly what a\n * hand-written call would have produced.\n *\n * `transaction_id` stays under its own name here. Meta's pixel documents no\n * order id among its object properties. `order_id` exists on the Conversions\n * API and not on this surface, so translating it would be inventing a slot.\n */\nfunction pixelParams(data: EventData): Record<string, unknown> {\n const { items, ...rest } = data;\n const params: Record<string, unknown> = { ...rest };\n\n if (items) params.contents = contents(items);\n\n return params;\n}\n\n/**\n * Meta's pixel has one consent switch where this library carries Google's\n * four signals, so the four are collapsed into it.\n *\n * The collapse is the strictest declared answer: any `denied` produces\n * `revoke`, and `grant` needs every declared signal to be `granted`. There is\n * no field to partition, so this is the only reduction that cannot widen a\n * denial, and widening is the one thing a projection must never do.\n *\n * A host that wants Meta to follow only its advertising category, and not its\n * analytics one, decides that before `createTracking` by leaving this\n * provider out, which is what `@tracklane/consent` exists to do.\n */\nfunction collapse(state: ConsentState): 'grant' | 'revoke' | undefined {\n const signals = [\n state.ad_storage,\n state.analytics_storage,\n state.ad_user_data,\n state.ad_personalization,\n ].filter((signal) => signal !== undefined);\n\n // Nothing declared produces no command. Reading silence as either answer\n // puts words in the visitor's mouth.\n if (signals.length === 0) return undefined;\n\n return signals.every((signal) => signal === 'granted') ? 'grant' : 'revoke';\n}\n\n/**\n * The Meta pixel in the browser, through `fbq`.\n *\n * **This provider does not load Meta's pixel.** The base code belongs on the\n * page, installed the way\n * [Meta documents it](https://developers.facebook.com/docs/meta-pixel/get-started):\n * a snippet in the HTML, or a tag manager. This library talks to the pixel\n * that is already there; it never injects a third-party script and never\n * issues `fbq('init')`.\n *\n * Three consequences follow from that, and all three are Meta's rules rather\n * than ours:\n *\n * - **There is no `identify` here.** Meta accepts advanced matching only in\n * the base code (\"be sure to place advanced matching parameters in the\n * pixel base code or the values will not be treated as manual advanced\n * matching values\"), and the base code is yours. Put `em`, `ph`, `fn`, `ln`\n * in your own `fbq('init')`, where the pixel normalises and hashes them for\n * you. On the server, where there is no snippet, the same identity travels\n * in `context.user` and is hashed by the adapter.\n * - **The initial consent declaration is not ours.** Meta wants\n * `fbq('consent', 'revoke')` before `init`, and by the time you hold what\n * `createTracking` returns that moment has passed. Ours is every\n * declaration after it, through `consent()`.\n * - **Limited Data Use is not ours either.** `fbq('dataProcessingOptions', …)`\n * is documented as running before `init`. It belongs in your snippet. The\n * server half, which has no snippet, carries it on its factory.\n *\n * **There is no pixel id here**, and that is deliberate rather than an\n * omission. `fbq('track')` reaches every pixel the page initialised, so an id\n * passed in would address nothing and the parameter would promise something\n * this adapter cannot deliver. Which pixels exist is declared by your snippet,\n * in `fbq('init')`.\n *\n * @param config - Event name overrides, merged over the built-in map.\n * @returns A provider for `createTracking` from `tracklane/browser`.\n *\n * @example\n * ```ts\n * import { createTracking, meta } from 'tracklane/browser';\n *\n * // Meta's own base code is already in the page, and it named the pixel.\n * const { track } = createTracking({ providers: [meta()] });\n *\n * // fbq('track', 'Purchase', { value: 49.9, currency: 'BRL' }, { eventID: 'T-1' })\n * track('purchase', { value: 49.9, currency: 'BRL' }, { dedupId: 'T-1' });\n * ```\n */\nexport function meta(config: MetaBrowserConfig = {}): BrowserProvider {\n return {\n name: 'meta',\n // An event Meta has no standard name for is still an event Meta accepts,\n // through `trackCustom`. Nothing is silently dropped.\n default: 'passthrough',\n events: { ...CANONICAL_EVENTS, ...config.events },\n\n track(name: string, data: EventData, options: TrackOptions): void {\n // `track`, which is what a hand-written call is, and the only form Meta\n // documents the deduplication object for.\n //\n // `trackSingle` exists and would address one pixel, the way GA4's\n // `send_to` does. It was used here and then abandoned: Meta publishes no\n // signature for `trackSingle` carrying `{ eventID }`, so the placement\n // that deduplication depends on had no contract behind it. It worked when\n // verified against a real pixel, and an undocumented shape that works\n // today can stop working with no error and no visible symptom — the\n // browser and server hits would simply both count.\n //\n // The cost is Meta's own: on a page with two pixels initialised, `track`\n // reaches both. That is what `fbq('track')` does by hand, so this\n // replicates it rather than deciding otherwise on the host's behalf.\n const command = STANDARD_EVENTS.includes(name) ? 'track' : 'trackCustom';\n const args: unknown[] = [command, name, pixelParams(data)];\n\n // Meta's key is `eventID` here and `event_id` on the Conversions API,\n // matched together with the event name. The library never invents the\n // value: only one the host already owns can match on both sides.\n if (options.dedupId !== undefined) args.push({ eventID: options.dedupId });\n\n push(...args);\n },\n\n consent(_command: 'default' | 'update', state: ConsentState): void {\n // Meta has one consent command and no equivalent of Google's\n // default/update split, so the command maps to nothing. What is\n // declared is the state, and that is what is forwarded.\n const answer = collapse(state);\n if (answer) push('consent', answer);\n },\n };\n}\n","// Through the public entry point, exactly as a provider written elsewhere\n// would import it. A provider that needs something not exported here is\n// telling us the contract is incomplete, which is a conversation rather than\n// a reason to reach inside.\nimport type {\n BrowserProvider,\n ConsentState,\n EventBinding,\n EventData,\n TrackOptions,\n UserData,\n} from '../index.js';\n\n/** The slice of `posthog-js` this adapter calls. */\ninterface PosthogSdk {\n capture: (event: string, properties?: EventData) => void;\n identify: (distinctId: string) => void;\n setPersonProperties: (properties: Record<string, unknown>) => void;\n opt_in_capturing: () => void;\n opt_out_capturing: () => void;\n}\n\ninterface PosthogWindow {\n posthog?: PosthogSdk;\n}\n\n/** Optional configuration for the PostHog browser provider. */\nexport interface PosthogBrowserConfig {\n /**\n * How this project spells each canonical event. Rarely needed: `capture()`\n * accepts any string, so the canonical vocabulary already is PostHog's.\n */\n events?: Record<string, EventBinding<string>>;\n}\n\n// Server rendering reaches this module with no browser at all, where\n// `window` is a ReferenceError rather than `undefined`.\nfunction posthogWindow(): PosthogWindow | undefined {\n return typeof window === 'undefined' ? undefined : (window as unknown as PosthogWindow);\n}\n\n/**\n * Returns the vendor SDK, or fails the way a hand-written call would.\n *\n * Calling `posthog.capture(...)` on a page without PostHog's snippet throws,\n * and this library replicates that rather than swallowing it: a silent no-op\n * is how a host ends up believing it is measuring something.\n */\nfunction sdk(): PosthogSdk {\n const instance = posthogWindow()?.posthog;\n\n if (!instance) {\n throw new Error(\n \"posthog: window.posthog is not defined. Add PostHog's snippet to the page before creating tracking.\",\n );\n }\n\n return instance;\n}\n\n/**\n * PostHog in the browser, through `posthog-js`.\n *\n * **This provider does not load PostHog's snippet.** It belongs on the page,\n * installed the way PostHog documents it: a snippet in the HTML, or a tag\n * manager. This library talks to the instance that is already there; it\n * never injects a third-party script into your page.\n *\n * @param config - Rarely needed event name overrides.\n * @returns A provider for `createTracking` from `tracklane/browser`.\n *\n * @example\n * ```ts\n * import { createTracking, posthog } from 'tracklane/browser';\n *\n * // PostHog's own snippet is already in the page.\n * const { track } = createTracking({ providers: [posthog()] });\n *\n * track('purchase', { transaction_id: 'T-1', value: 49.9, currency: 'BRL' });\n * ```\n */\nexport function posthog(config: PosthogBrowserConfig = {}): BrowserProvider {\n const { events } = config;\n\n return {\n name: 'posthog',\n default: 'passthrough',\n ...(events ? { events } : {}),\n\n track(name: string, data: EventData, _options: TrackOptions): void {\n // capture() accepts any string as the event name and documents no\n // browser-to-server deduplication key, so dedupId maps nowhere — the\n // same gap GA4 has, for the same reason: there is no field to put it\n // in.\n sdk().capture(name, data);\n },\n\n identify(user: UserData, traits?: Record<string, unknown>): void {\n // distinct_id is PostHog's only identity slot. email, phone and names\n // have no separate slot the way Meta's user_data has one for each —\n // they map to nothing here, exactly the gap GA4 has for the same\n // fields.\n if (user.userId !== undefined) sdk().identify(user.userId);\n // setPersonProperties works against whichever identity is already\n // current, so it needs no id of its own — the same shape as GA4's\n // separate `set user_properties` push.\n if (traits) sdk().setPersonProperties(traits);\n },\n\n consent(_command: 'default' | 'update', state: ConsentState): void {\n // opt_in_capturing/opt_out_capturing is one binary switch, local SDK\n // state, not a per-signal API — nothing like Google's four commands.\n // analytics_storage is the one signal that actually describes what\n // this toggle governs (whether PostHog stores and sends analytics\n // events at all), so it is the one signal this collapses onto. The\n // other three have no honest PostHog equivalent; folding them in too\n // would risk an ad-only denial silently switching off analytics\n // capture the visitor never objected to, so they are left alone.\n const { analytics_storage } = state;\n\n if (analytics_storage === 'denied') {\n sdk().opt_out_capturing();\n } else if (analytics_storage === 'granted') {\n sdk().opt_in_capturing();\n }\n // Absent: no command fires. Whatever posture the page's own PostHog\n // snippet already set — opted in by default, or opted out through\n // `opt_out_capturing_by_default` — stands, rather than being guessed at.\n },\n };\n}\n","import { assertUniqueProviders, makeReporter, messageOf, resolveEvent } from './dispatch.js';\n\nexport { type Ga4BrowserConfig, ga4 } from './providers/ga4.browser.js';\nexport { type MetaBrowserConfig, meta } from './providers/meta.browser.js';\nexport { type PosthogBrowserConfig, posthog } from './providers/posthog.browser.js';\n\nimport type {\n ConsentState,\n EventBinding,\n EventData,\n ProviderDefault,\n TrackingError,\n UserData,\n} from './vocabulary.js';\n\n/**\n * Everything the browser needs beyond the event itself.\n *\n * There is one field, and that is the point: in the browser each vendor tag\n * reads its own cookies and holds its own session, so the only thing left\n * for the host to supply is the value that ties this hit to a server-side\n * one.\n */\nexport interface TrackOptions {\n /**\n * The value that lets a vendor recognise this hit and a server-side hit as\n * one event. The library never invents it: the browser and the server do\n * not share a process, so only a value the host already owns (an order id)\n * can match on both sides.\n */\n dedupId?: string;\n}\n\n/**\n * A destination for browser events.\n *\n * This is the whole contract. A provider written outside this package\n * implements it exactly as the providers shipped here do.\n */\nexport interface BrowserProvider<Target = string> {\n /** The vendor's fixed name. Registering one twice is an error. */\n name: string;\n /** What happens to a canonical event this vendor has no binding for. */\n default: ProviderDefault;\n /** How this vendor spells each canonical event. */\n events?: Record<string, EventBinding<Target>>;\n /**\n * Loads the vendor tag, at creation.\n *\n * **Make it idempotent.** A host that rebuilds its tracking, as\n * `@tracklane/consent` does on every consent answer, creates again and\n * installs again, including providers that were already there.\n */\n install?(): void | Promise<void>;\n /** Sends one event to this vendor. Throwing here reaches `onError`, not the caller. */\n track(name: Target, data: EventData, options: TrackOptions): void;\n /** Absent where the vendor keeps no user of its own. */\n identify?(user: UserData, traits?: Record<string, unknown>): void;\n /** Absent where the vendor documents no consent command. */\n consent?(command: 'default' | 'update', state: ConsentState): void;\n}\n\n/** Options accepted by {@link createTracking}. */\nexport interface BrowserTrackingOptions {\n /** The vendors to send every event to. Registering the same `name` twice throws. */\n providers: readonly BrowserProvider[];\n /**\n * The one diagnostic channel. Without it the library is silent, exactly as\n * a hand-written vendor call is.\n */\n onError?: (error: TrackingError) => void;\n}\n\n/** What {@link createTracking} returns. */\nexport interface BrowserTracking {\n /** Sends one event to every configured vendor. Never throws. */\n track(name: string, data?: EventData, options?: TrackOptions): void;\n /** Forwards the user to the vendors that hold one, and to nobody else. */\n identify(user: UserData, traits?: Record<string, unknown>): void;\n /**\n * Forwards a consent declaration to every vendor that documents a command\n * for it. It never decides, stores or withholds anything.\n */\n consent(command: 'default' | 'update', state: ConsentState): void;\n}\n\n/**\n * Creates the browser tracking surface.\n *\n * Providers install eagerly, which is what pasting a vendor snippet does and\n * what lets each vendor record its own initial page view. Every vendor\n * shipped here queues its own commands until its script lands, so nothing is\n * lost to loading and this library needs no queue of its own.\n *\n * @param options - The providers to send to, and an optional error channel.\n * @returns The `track`, `identify` and `consent` calls.\n *\n * @example\n * ```ts\n * import { createTracking, ga4 } from 'tracklane/browser';\n *\n * const { track } = createTracking({ providers: [ga4('G-XXXXXXX')] });\n * track('purchase', { transaction_id: 'T-1', value: 49.9, currency: 'BRL' });\n * ```\n */\nexport function createTracking(options: BrowserTrackingOptions): BrowserTracking {\n const { providers } = options;\n assertUniqueProviders(providers);\n\n const report = makeReporter(options.onError);\n\n const fail = (provider: string, event: string, cause: unknown): void => {\n report({ provider, event, severity: 'error', message: messageOf(cause), cause });\n };\n\n for (const provider of providers) {\n try {\n const installing = provider.install?.();\n // An async loader that rejects would otherwise become an unhandled\n // rejection, the silent failure this channel exists to prevent.\n void Promise.resolve(installing).catch((cause: unknown) => {\n fail(provider.name, 'install', cause);\n });\n } catch (cause) {\n fail(provider.name, 'install', cause);\n }\n }\n\n return {\n track(name, data = {}, trackOptions = {}) {\n for (const provider of providers) {\n const target = resolveEvent(provider, name);\n if (target === undefined) continue;\n\n try {\n provider.track(target, data, trackOptions);\n } catch (cause) {\n fail(provider.name, name, cause);\n }\n }\n },\n\n identify(user, traits) {\n for (const provider of providers) {\n try {\n provider.identify?.(user, traits);\n } catch (cause) {\n fail(provider.name, 'identify', cause);\n }\n }\n },\n\n consent(command, state) {\n for (const provider of providers) {\n try {\n provider.consent?.(command, state);\n } catch (cause) {\n fail(provider.name, 'consent', cause);\n }\n }\n },\n };\n}\n"],"mappings":";;AA4BA,SAAS,aAAqC;CAC5C,OAAO,OAAO,WAAW,cAAc,KAAA,IAAa;AACtD;;;;;;;;AASA,SAASA,OAAK,GAAG,MAAuB;CACtC,MAAM,OAAO,WAAW,CAAC,EAAE;CAE3B,IAAI,CAAC,MACH,MAAM,IAAI,MACR,yFACF;CAGF,KAAK,GAAG,IAAI;AACd;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,SAAgB,IAAI,eAAuB,SAA2B,CAAC,GAAoB;CACzF,MAAM,EAAE,WAAW;CAEnB,OAAO;EACL,MAAM;EAGN,SAAS;EACT,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;EAE3B,MAAM,MAAc,MAAiB,UAA8B;GASjE,OAAK,SAAS,MAAM;IAAE,GAAG;IAAM,SAAS;GAAc,CAAC;EACzD;EAEA,SAAS,MAAgB,QAAwC;GAG/D,IAAI,KAAK,WAAW,KAAA,GAAW,OAAK,OAAO,EAAE,SAAS,KAAK,OAAO,CAAC;GACnE,IAAI,QAAQ,OAAK,OAAO,mBAAmB,MAAM;EACnD;EAEA,QAAQ,SAA+B,OAA2B;GAIhE,OAAK,WAAW,SAAS,KAAK;EAChC;CACF;AACF;;;;;;;;;;;AC3FA,MAAM,kBAAqC;CACzC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;;;;;;;;;;AAgBA,MAAM,mBAAyD;CAC7D,WAAW;CACX,WAAW;CACX,QAAQ;CACR,aAAa;CACb,iBAAiB;CACjB,gBAAgB;CAChB,kBAAkB;CAClB,UAAU;CACV,SAAS;CACT,eAAe;AACjB;AAeA,SAAS,YAAmC;CAC1C,OAAO,OAAO,WAAW,cAAc,KAAA,IAAa;AACtD;;;;;;;;AASA,SAAS,KAAK,GAAG,MAAuB;CACtC,MAAM,MAAM,UAAU,CAAC,EAAE;CAEzB,IAAI,CAAC,KACH,MAAM,IAAI,MACR,yFACF;CAGF,IAAI,GAAG,IAAI;AACb;;;;;;;;;AAUA,SAAS,SAAS,OAAwD;CACxE,OAAO,MAAM,KAAK,SAAS;EACzB,MAAM,UAAmC,CAAC;EAE1C,IAAI,KAAK,YAAY,KAAA,GAAW,QAAQ,KAAK,KAAK;EAClD,IAAI,KAAK,aAAa,KAAA,GAAW,QAAQ,WAAW,KAAK;EACzD,IAAI,KAAK,UAAU,KAAA,GAAW,QAAQ,aAAa,KAAK;EAExD,OAAO;CACT,CAAC;AACH;;;;;;;;;;;AAYA,SAAS,YAAY,MAA0C;CAC7D,MAAM,EAAE,OAAO,GAAG,SAAS;CAC3B,MAAM,SAAkC,EAAE,GAAG,KAAK;CAElD,IAAI,OAAO,OAAO,WAAW,SAAS,KAAK;CAE3C,OAAO;AACT;;;;;;;;;;;;;;AAeA,SAAS,SAAS,OAAqD;CACrE,MAAM,UAAU;EACd,MAAM;EACN,MAAM;EACN,MAAM;EACN,MAAM;CACR,CAAC,CAAC,QAAQ,WAAW,WAAW,KAAA,CAAS;CAIzC,IAAI,QAAQ,WAAW,GAAG,OAAO,KAAA;CAEjC,OAAO,QAAQ,OAAO,WAAW,WAAW,SAAS,IAAI,UAAU;AACrE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,SAAgB,KAAK,SAA4B,CAAC,GAAoB;CACpE,OAAO;EACL,MAAM;EAGN,SAAS;EACT,QAAQ;GAAE,GAAG;GAAkB,GAAG,OAAO;EAAO;EAEhD,MAAM,MAAc,MAAiB,SAA6B;GAgBhE,MAAM,OAAkB;IADR,gBAAgB,SAAS,IAAI,IAAI,UAAU;IACzB;IAAM,YAAY,IAAI;GAAC;GAKzD,IAAI,QAAQ,YAAY,KAAA,GAAW,KAAK,KAAK,EAAE,SAAS,QAAQ,QAAQ,CAAC;GAEzE,KAAK,GAAG,IAAI;EACd;EAEA,QAAQ,UAAgC,OAA2B;GAIjE,MAAM,SAAS,SAAS,KAAK;GAC7B,IAAI,QAAQ,KAAK,WAAW,MAAM;EACpC;CACF;AACF;;;ACrOA,SAAS,gBAA2C;CAClD,OAAO,OAAO,WAAW,cAAc,KAAA,IAAa;AACtD;;;;;;;;AASA,SAAS,MAAkB;CACzB,MAAM,WAAW,cAAc,CAAC,EAAE;CAElC,IAAI,CAAC,UACH,MAAM,IAAI,MACR,qGACF;CAGF,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,QAAQ,SAA+B,CAAC,GAAoB;CAC1E,MAAM,EAAE,WAAW;CAEnB,OAAO;EACL,MAAM;EACN,SAAS;EACT,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;EAE3B,MAAM,MAAc,MAAiB,UAA8B;GAKjE,IAAI,CAAC,CAAC,QAAQ,MAAM,IAAI;EAC1B;EAEA,SAAS,MAAgB,QAAwC;GAK/D,IAAI,KAAK,WAAW,KAAA,GAAW,IAAI,CAAC,CAAC,SAAS,KAAK,MAAM;GAIzD,IAAI,QAAQ,IAAI,CAAC,CAAC,oBAAoB,MAAM;EAC9C;EAEA,QAAQ,UAAgC,OAA2B;GASjE,MAAM,EAAE,sBAAsB;GAE9B,IAAI,sBAAsB,UACxB,IAAI,CAAC,CAAC,kBAAkB;QACnB,IAAI,sBAAsB,WAC/B,IAAI,CAAC,CAAC,iBAAiB;EAK3B;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;ACzBA,SAAgB,eAAe,SAAkD;CAC/E,MAAM,EAAE,cAAc;CACtB,sBAAsB,SAAS;CAE/B,MAAM,SAAS,aAAa,QAAQ,OAAO;CAE3C,MAAM,QAAQ,UAAkB,OAAe,UAAyB;EACtE,OAAO;GAAE;GAAU;GAAO,UAAU;GAAS,SAAS,UAAU,KAAK;GAAG;EAAM,CAAC;CACjF;CAEA,KAAK,MAAM,YAAY,WACrB,IAAI;EACF,MAAM,aAAa,SAAS,UAAU;EAGtC,QAAa,QAAQ,UAAU,CAAC,CAAC,OAAO,UAAmB;GACzD,KAAK,SAAS,MAAM,WAAW,KAAK;EACtC,CAAC;CACH,SAAS,OAAO;EACd,KAAK,SAAS,MAAM,WAAW,KAAK;CACtC;CAGF,OAAO;EACL,MAAM,MAAM,OAAO,CAAC,GAAG,eAAe,CAAC,GAAG;GACxC,KAAK,MAAM,YAAY,WAAW;IAChC,MAAM,SAAS,aAAa,UAAU,IAAI;IAC1C,IAAI,WAAW,KAAA,GAAW;IAE1B,IAAI;KACF,SAAS,MAAM,QAAQ,MAAM,YAAY;IAC3C,SAAS,OAAO;KACd,KAAK,SAAS,MAAM,MAAM,KAAK;IACjC;GACF;EACF;EAEA,SAAS,MAAM,QAAQ;GACrB,KAAK,MAAM,YAAY,WACrB,IAAI;IACF,SAAS,WAAW,MAAM,MAAM;GAClC,SAAS,OAAO;IACd,KAAK,SAAS,MAAM,YAAY,KAAK;GACvC;EAEJ;EAEA,QAAQ,SAAS,OAAO;GACtB,KAAK,MAAM,YAAY,WACrB,IAAI;IACF,SAAS,UAAU,SAAS,KAAK;GACnC,SAAS,OAAO;IACd,KAAK,SAAS,MAAM,WAAW,KAAK;GACtC;EAEJ;CACF;AACF"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { i as TrackOptions, n as BrowserTracking, r as BrowserTrackingOptions, t as BrowserProvider } from "./browser-usMUxJhU.js";
|
|
3
|
-
import { a as ServerTrackingOptions, i as ServerTracking, n as ResolvedContext, r as ServerProvider, t as EventContext } from "./server-DRda44Tb.js";
|
|
1
|
+
import { C as ProviderDefault, S as EventItem, T as UserData, b as EventBinding, f as EventContext, g as ServerTrackingOptions, h as ServerTracking, i as TrackOptions, m as ServerProvider, n as BrowserTracking, p as ResolvedContext, r as BrowserTrackingOptions, t as BrowserProvider, v as ActionSource, w as TrackingError, x as EventData, y as ConsentState } from "./browser-FeV5oRFZ.js";
|
|
4
2
|
export type { ActionSource, BrowserProvider, BrowserTracking, BrowserTrackingOptions, ConsentState, EventBinding, EventContext, EventData, EventItem, ProviderDefault, ResolvedContext, ServerProvider, ServerTracking, ServerTrackingOptions, TrackOptions, TrackingError, UserData };
|
package/dist/server.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { EventContext, type Ga4ServerConfig, type Ga4ServerCredentials, ResolvedContext, ServerProvider, ServerTracking, ServerTrackingOptions, createTracking, ga4 };
|
|
1
|
+
import { A as MetaServerCredentials, D as PosthogServerCredentials, E as PosthogServerConfig, M as Ga4ServerConfig, N as Ga4ServerCredentials, O as posthog, P as ga4, _ as createTracking, f as EventContext, g as ServerTrackingOptions, h as ServerTracking, j as meta, k as MetaServerConfig, m as ServerProvider, p as ResolvedContext } from "./browser-FeV5oRFZ.js";
|
|
2
|
+
export { EventContext, type Ga4ServerConfig, type Ga4ServerCredentials, type MetaServerConfig, type MetaServerCredentials, type PosthogServerConfig, type PosthogServerCredentials, ResolvedContext, ServerProvider, ServerTracking, ServerTrackingOptions, createTracking, ga4, meta, posthog };
|