tracklane 0.7.0 → 0.8.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.
@@ -53,10 +53,86 @@ function consentBody(state) {
53
53
  return Object.keys(body).length > 0 ? body : void 0;
54
54
  }
55
55
  /**
56
+ * The Measurement Protocol's documented limits: "User property names must be
57
+ * 24 characters or fewer", "User property values must be 36 characters or
58
+ * fewer", and at most 25 user properties.
59
+ */
60
+ const MAX_PROPERTY_NAME = 24;
61
+ const MAX_PROPERTY_VALUE = 36;
62
+ const MAX_PROPERTIES = 25;
63
+ /**
64
+ * Traits as GA4 user properties. What GA4 cannot hold is left out and
65
+ * reported by name, never replaced: a clear or a truncation would be a value
66
+ * the host did not send. `null` is left out too, because clearing through
67
+ * this protocol is not verified yet (ADR-0014).
68
+ */
69
+ function userProperties(traits, traitsAt, report) {
70
+ if (!traits) return void 0;
71
+ const built = {};
72
+ const unclearable = [];
73
+ const tooLong = [];
74
+ for (const [name, value] of Object.entries(traits)) if (value === null) unclearable.push(name);
75
+ else if (name.length > MAX_PROPERTY_NAME || typeof value === "string" && value.length > MAX_PROPERTY_VALUE) tooLong.push(name);
76
+ else built[name] = {
77
+ value,
78
+ ...traitsAt === void 0 ? {} : { timestamp_micros: traitsAt * 1e3 }
79
+ };
80
+ if (unclearable.length > 0) report(`user properties not cleared, because clearing one through the Measurement Protocol is not verified: ${unclearable.join(", ")}`);
81
+ if (tooLong.length > 0) report(`user properties left out, over GA4's ${MAX_PROPERTY_NAME}-character name or ${MAX_PROPERTY_VALUE}-character value limit: ${tooLong.join(", ")}`);
82
+ if (Object.keys(built).length > MAX_PROPERTIES) report(`more than ${MAX_PROPERTIES} user properties, GA4's limit: all were sent, and Google decides which it keeps`);
83
+ return Object.keys(built).length > 0 ? built : void 0;
84
+ }
85
+ async function sha256$2(value) {
86
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
87
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
88
+ }
89
+ /**
90
+ * Google's documented normalisation for user-provided data, which differs
91
+ * from Meta's and LinkedIn's: Gmail addresses lose the dots before `@`, and
92
+ * phones keep a leading `+`.
93
+ */
94
+ function normaliseEmail$1(value) {
95
+ const email = value.replace(/\s/g, "").toLowerCase();
96
+ const [local, domain] = email.split("@");
97
+ if (local === void 0 || domain === void 0) return email;
98
+ return domain === "gmail.com" || domain === "googlemail.com" ? `${local.replace(/\./g, "")}@${domain}` : email;
99
+ }
100
+ function normalisePhone$1(value) {
101
+ const digits = value.replace(/\D/g, "");
102
+ return digits ? `+${digits}` : "";
103
+ }
104
+ function normaliseName$1(value) {
105
+ return value.replace(/[^\p{L}\s]/gu, "").toLowerCase().trim();
106
+ }
107
+ async function userData$2(user) {
108
+ if (!user) return void 0;
109
+ const built = {};
110
+ const hashed = async (raw, normalise) => {
111
+ const normalised = raw === void 0 ? "" : normalise(raw);
112
+ return normalised ? sha256$2(normalised) : void 0;
113
+ };
114
+ const email = await hashed(user.email, normaliseEmail$1);
115
+ if (email) built.sha256_email_address = [email];
116
+ const phone = await hashed(user.phone, normalisePhone$1);
117
+ if (phone) built.sha256_phone_number = [phone];
118
+ const firstName = await hashed(user.firstName, normaliseName$1);
119
+ const lastName = await hashed(user.lastName, normaliseName$1);
120
+ if (firstName || lastName) built.address = [{
121
+ ...firstName ? { sha256_first_name: firstName } : {},
122
+ ...lastName ? { sha256_last_name: lastName } : {}
123
+ }];
124
+ return Object.keys(built).length > 0 ? built : void 0;
125
+ }
126
+ /**
56
127
  * Google Analytics 4 on the server, through the Measurement Protocol.
57
128
  *
129
+ * **`identify` sends user properties with no event**, which Google
130
+ * documents. With no user property to send it sends nothing, because each
131
+ * `track` already carries `user_id`.
132
+ *
58
133
  * @param credentials - The measurement id and API secret.
59
- * @param config - Rarely needed event name overrides.
134
+ * @param config - Rarely needed event name overrides, and the opt-in for
135
+ * user-provided data.
60
136
  * @returns A provider for `createTracking` from `tracklane/server`.
61
137
  *
62
138
  * @example
@@ -72,46 +148,70 @@ function consentBody(state) {
72
148
  */
73
149
  function ga4(credentials, config = {}) {
74
150
  const { measurementId, apiSecret } = credentials;
151
+ const clientIdOf = (context) => {
152
+ const clientId = visitorId(context.cookies);
153
+ if (!clientId) throw new Error("ga4: cannot build client_id, no _ga cookie among the cookies passed in context. Ensure GA4's browser tag has set it first.");
154
+ return clientId;
155
+ };
156
+ const person = async (context, report) => {
157
+ const body = {
158
+ client_id: clientIdOf(context),
159
+ timestamp_micros: context.timestamp * 1e3
160
+ };
161
+ if (context.user?.userId !== void 0) body.user_id = context.user.userId;
162
+ const properties = userProperties(context.traits, context.traitsAt, report);
163
+ if (properties) body.user_properties = properties;
164
+ if (config.userProvidedData) {
165
+ const data = await userData$2(context.user);
166
+ if (data) body.user_data = data;
167
+ }
168
+ const consent = consentBody(context.consent);
169
+ if (consent) body.consent = consent;
170
+ return body;
171
+ };
172
+ const send = async (body, signal) => {
173
+ const url = `${ENDPOINT}?measurement_id=${encodeURIComponent(measurementId)}&api_secret=${encodeURIComponent(apiSecret)}`;
174
+ const response = await fetch(url, {
175
+ method: "POST",
176
+ headers: { "content-type": "application/json" },
177
+ body: JSON.stringify(body),
178
+ ...signal ? { signal } : {}
179
+ });
180
+ if (!response.ok) {
181
+ const text = await response.text().catch(() => "");
182
+ throw new VendorResponseError(`ga4: the Measurement Protocol answered ${response.status}`, {
183
+ provider: "ga4",
184
+ status: response.status,
185
+ body: text
186
+ });
187
+ }
188
+ };
75
189
  return {
76
190
  name: "ga4",
77
191
  default: "passthrough",
78
192
  ...config.events ? { events: config.events } : {},
79
193
  async track(name, data, context, report) {
80
- const clientId = visitorId(context.cookies);
81
- if (!clientId) throw new Error("ga4: cannot build client_id, no _ga cookie among the cookies passed in context. Ensure GA4's browser tag has set it first.");
194
+ const body = await person(context, report);
82
195
  const session = sessionId(context.cookies, measurementId);
83
196
  if (!session) report("no session cookie (_ga_<container>): Google will accept this event and it will not appear in any report");
84
197
  const params = { ...data };
85
198
  if (session) {
86
199
  params.session_id = session;
87
- params.engagement_time_msec = 1;
200
+ params.engagement_time_msec ??= 1;
88
201
  }
89
- const body = {
90
- client_id: clientId,
91
- timestamp_micros: context.timestamp * 1e3,
202
+ await send({
203
+ ...body,
92
204
  events: [{
93
205
  name,
94
206
  params
95
207
  }]
96
- };
97
- if (context.user?.userId !== void 0) body.user_id = context.user.userId;
98
- if (context.traits) body.user_properties = Object.fromEntries(Object.entries(context.traits).map(([name, value]) => [name, { value }]));
99
- const consent = consentBody(context.consent);
100
- if (consent) body.consent = consent;
101
- const url = `${ENDPOINT}?measurement_id=${encodeURIComponent(measurementId)}&api_secret=${encodeURIComponent(apiSecret)}`;
102
- const response = await fetch(url, {
103
- method: "POST",
104
- headers: { "content-type": "application/json" },
105
- body: JSON.stringify(body)
106
- });
107
- if (!response.ok) {
108
- const body = await response.text().catch(() => "");
109
- throw new VendorResponseError(`ga4: the Measurement Protocol answered ${response.status}`, {
110
- provider: "ga4",
111
- status: response.status,
112
- body
113
- });
114
- }
208
+ }, context.signal);
209
+ },
210
+ async identify(context, report) {
211
+ if (context.user?.userId === void 0) throw new Error("ga4: identify needs user.userId");
212
+ const body = await person(context, report);
213
+ if (!body.user_properties) return;
214
+ await send(body, context.signal);
115
215
  }
116
216
  };
117
217
  }
@@ -199,7 +299,8 @@ function linkedin(credentials, config) {
199
299
  "Linkedin-Version": apiVersion,
200
300
  "X-Restli-Protocol-Version": "2.0.0"
201
301
  },
202
- body: JSON.stringify(event)
302
+ body: JSON.stringify(event),
303
+ ...context.signal ? { signal: context.signal } : {}
203
304
  });
204
305
  if (!response.ok) {
205
306
  const body = await response.text().catch(() => "");
@@ -425,7 +526,8 @@ function meta(credentials, config = {}) {
425
526
  const response = await fetch(`${GRAPH}/${apiVersion}/${encodeURIComponent(pixelId)}/events`, {
426
527
  method: "POST",
427
528
  headers: { "content-type": "application/x-www-form-urlencoded" },
428
- body
529
+ body,
530
+ ...context.signal ? { signal: context.signal } : {}
429
531
  });
430
532
  if (!response.ok) {
431
533
  const body = await response.text().catch(() => "");
@@ -483,6 +585,31 @@ function anonymousId(cookies, apiKey) {
483
585
  */
484
586
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
485
587
  /**
588
+ * PostHog documents that it "shortens any `distinct_id` longer than 200
589
+ * characters" and ingests the event under the shortened id. Sent as given,
590
+ * because refusing would drop an event the vendor accepts; reported, because
591
+ * two ids sharing a prefix become one person and only the host can fix that.
592
+ */
593
+ const MAX_DISTINCT_ID = 200;
594
+ function checkLength(id, report) {
595
+ if (id.length > MAX_DISTINCT_ID) report(`distinct_id is longer than ${MAX_DISTINCT_ID} characters: PostHog truncates it, so two ids sharing that prefix become one person`);
596
+ }
597
+ /**
598
+ * Traits with a value go to `$set`; a trait set to `null` means "no longer
599
+ * known", which PostHog spells as a name in `$unset`.
600
+ */
601
+ function personProperties(traits) {
602
+ if (!traits) return {};
603
+ const set = {};
604
+ const unset = [];
605
+ for (const [name, value] of Object.entries(traits)) if (value === null) unset.push(name);
606
+ else set[name] = value;
607
+ return {
608
+ ...Object.keys(set).length > 0 ? { $set: set } : {},
609
+ ...unset.length > 0 ? { $unset: unset } : {}
610
+ };
611
+ }
612
+ /**
486
613
  * PostHog on the server, through the Capture API.
487
614
  *
488
615
  * **Deduplicating a retried webhook takes two things, not one.** `dedupId`
@@ -496,6 +623,12 @@ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-
496
623
  * Deduplication is also eventual, happening during a background merge, so both
497
624
  * rows are visible for a while and a test that counts immediately will fail.
498
625
  *
626
+ * **`identify` sends one `$identify`**, with `$set` and `$unset` from the
627
+ * traits. It merges a visitor into the user only when the context carries
628
+ * `anonymousId`, never from the cookie: a merge is irreversible, and PostHog
629
+ * reports a refused one only as an ingestion warning in its own interface.
630
+ * Identified events are billed with person processing.
631
+ *
499
632
  * @param credentials - The project API key, and the ingestion host for
500
633
  * anything other than PostHog's default US Cloud instance.
501
634
  * @param config - Rarely needed event name overrides.
@@ -521,39 +654,62 @@ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-
521
654
  */
522
655
  function posthog(credentials, config = {}) {
523
656
  const { apiKey, host = DEFAULT_HOST } = credentials;
657
+ const send = async (body, signal) => {
658
+ const response = await fetch(`${host}/i/v0/e/`, {
659
+ method: "POST",
660
+ headers: { "content-type": "application/json" },
661
+ body: JSON.stringify({
662
+ api_key: apiKey,
663
+ ...body
664
+ }),
665
+ ...signal ? { signal } : {}
666
+ });
667
+ if (!response.ok) {
668
+ const text = await response.text().catch(() => "");
669
+ throw new VendorResponseError(`posthog: the capture endpoint answered ${response.status}`, {
670
+ provider: "posthog",
671
+ status: response.status,
672
+ body: text
673
+ });
674
+ }
675
+ };
524
676
  return {
525
677
  name: "posthog",
526
678
  default: "passthrough",
527
679
  ...config.events ? { events: config.events } : {},
528
680
  async track(name, data, context, report) {
529
- const id = context.user?.userId ?? anonymousId(context.cookies, apiKey);
530
- if (!id) throw new Error("posthog: cannot resolve distinct_id — no user.userId in context and no ph_<api_key>_posthog cookie among the cookies passed in context. Pass user.userId once the visitor is known, or ensure PostHog's browser tag has set its cookie first.");
531
- const properties = { ...data };
532
- if (context.traits) properties.$set = context.traits;
681
+ const id = context.user?.userId ?? context.anonymousId ?? anonymousId(context.cookies, apiKey);
682
+ if (!id) throw new Error("posthog: cannot resolve distinct_id — no user.userId or anonymousId in context and no ph_<api_key>_posthog cookie among the cookies passed in context. Pass user.userId once the visitor is known, or ensure PostHog's browser tag has set its cookie first.");
683
+ checkLength(id, report);
684
+ const properties = {
685
+ ...data,
686
+ ...personProperties(context.traits)
687
+ };
533
688
  const dedup = {};
534
689
  if (context.dedupId !== void 0) if (UUID.test(context.dedupId)) dedup.uuid = context.dedupId;
535
690
  else report("dedupId is not a UUID, so it was not forwarded as uuid: PostHog deduplicates on that field and rejects other shapes, so this event can duplicate on a retry");
536
691
  const body = {
537
- api_key: apiKey,
538
692
  event: name,
539
693
  distinct_id: id,
540
694
  ...dedup,
541
695
  properties,
542
696
  timestamp: new Date(context.timestamp).toISOString()
543
697
  };
544
- const response = await fetch(`${host}/i/v0/e/`, {
545
- method: "POST",
546
- headers: { "content-type": "application/json" },
547
- body: JSON.stringify(body)
548
- });
549
- if (!response.ok) {
550
- const body = await response.text().catch(() => "");
551
- throw new VendorResponseError(`posthog: the capture endpoint answered ${response.status}`, {
552
- provider: "posthog",
553
- status: response.status,
554
- body
555
- });
556
- }
698
+ await send(body, context.signal);
699
+ },
700
+ async identify(context, report) {
701
+ const id = context.user?.userId;
702
+ if (!id) throw new Error("posthog: identify needs user.userId");
703
+ checkLength(id, report);
704
+ await send({
705
+ event: "$identify",
706
+ distinct_id: id,
707
+ properties: {
708
+ ...personProperties(context.traits),
709
+ ...context.anonymousId === void 0 ? {} : { $anon_distinct_id: context.anonymousId }
710
+ },
711
+ timestamp: new Date(context.timestamp).toISOString()
712
+ }, context.signal);
557
713
  }
558
714
  };
559
715
  }
@@ -572,20 +728,32 @@ function parseCookies(cookies) {
572
728
  }
573
729
  return parsed;
574
730
  }
731
+ function toEpoch(value) {
732
+ return value instanceof Date ? value.getTime() : value;
733
+ }
734
+ function resolveContext(context) {
735
+ const { cookies, timestamp, traitsAt, ...rest } = context;
736
+ return {
737
+ ...rest,
738
+ cookies: parseCookies(cookies),
739
+ timestamp: timestamp === void 0 ? Date.now() : toEpoch(timestamp),
740
+ ...traitsAt === void 0 ? {} : { traitsAt: toEpoch(traitsAt) }
741
+ };
742
+ }
575
743
  /**
576
744
  * Creates the server tracking surface.
577
745
  *
578
- * There is no `identify` here and nothing is held between calls: a server
579
- * instance is shared by every request, so a retained identity would attach
580
- * one visitor to another visitor's conversion. Identity travels with the
581
- * call, which is also what every conversion API expects.
746
+ * Nothing is held between calls: a server instance is shared by every
747
+ * request, so a retained identity would attach one visitor to another
748
+ * visitor's conversion. Identity travels with each call, `identify`
749
+ * included, which is also what every conversion API expects.
582
750
  *
583
751
  * There is no `consent` call either. Consent commands are properties of a
584
752
  * vendor's on-page tag, and a server request has no tag. The one vendor that
585
753
  * accepts consent server-side reads it from the context.
586
754
  *
587
755
  * @param options - The providers to send to, and an optional error channel.
588
- * @returns The `track` call.
756
+ * @returns The `track` and `identify` calls.
589
757
  *
590
758
  * @example
591
759
  * ```ts
@@ -602,25 +770,31 @@ function createTracking(options) {
602
770
  const { providers } = options;
603
771
  assertUniqueProviders(providers);
604
772
  const report = makeReporter(options.onError);
605
- return { async track(name, data = {}, context = {}) {
606
- const { cookies, timestamp, ...rest } = context;
607
- const resolved = {
608
- ...rest,
609
- cookies: parseCookies(cookies),
610
- timestamp: timestamp instanceof Date ? timestamp.getTime() : timestamp ?? Date.now()
611
- };
612
- const sends = providers.map(async (provider) => {
613
- const target = resolveEvent(provider, name, (message) => {
773
+ const settle = async (provider, event, send) => {
774
+ try {
775
+ await send((message) => {
614
776
  report({
615
777
  provider: provider.name,
616
- event: name,
778
+ event,
617
779
  severity: "warning",
618
780
  message
619
781
  });
620
782
  });
621
- if (target === void 0) return;
622
- try {
623
- await provider.track(target, data, resolved, (message) => {
783
+ } catch (cause) {
784
+ report({
785
+ provider: provider.name,
786
+ event,
787
+ severity: "error",
788
+ message: messageOf(cause),
789
+ cause
790
+ });
791
+ }
792
+ };
793
+ return {
794
+ async track(name, data = {}, context = {}) {
795
+ const resolved = resolveContext(context);
796
+ await Promise.all(providers.map(async (provider) => {
797
+ const target = resolveEvent(provider, name, (message) => {
624
798
  report({
625
799
  provider: provider.name,
626
800
  event: name,
@@ -628,20 +802,21 @@ function createTracking(options) {
628
802
  message
629
803
  });
630
804
  });
631
- } catch (cause) {
632
- report({
633
- provider: provider.name,
634
- event: name,
635
- severity: "error",
636
- message: messageOf(cause),
637
- cause
638
- });
639
- }
640
- });
641
- await Promise.all(sends);
642
- } };
805
+ if (target === void 0) return;
806
+ await settle(provider, name, (warn) => provider.track(target, data, resolved, warn));
807
+ }));
808
+ },
809
+ async identify(context) {
810
+ const resolved = resolveContext(context);
811
+ await Promise.all(providers.map((provider) => {
812
+ const identify = provider.identify?.bind(provider);
813
+ if (!identify) return void 0;
814
+ return settle(provider, "identify", (warn) => identify(resolved, warn));
815
+ }));
816
+ }
817
+ };
643
818
  }
644
819
  //#endregion
645
820
  export { ga4 as a, linkedin as i, posthog as n, meta as r, createTracking as t };
646
821
 
647
- //# sourceMappingURL=server-DnP7RtEn.js.map
822
+ //# sourceMappingURL=server-bgUN2MOi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-bgUN2MOi.js","names":["sha256","normaliseEmail","normalisePhone","normaliseName","userData","sha256","userData"],"sources":["../src/providers/ga4.server.ts","../src/providers/linkedin.server.ts","../src/providers/meta.server.ts","../src/providers/posthog.server.ts","../src/server.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 ConsentState,\n EventBinding,\n EventData,\n ResolvedContext,\n ServerProvider,\n UserData,\n} from '../index.js';\nimport { VendorResponseError } from '../index.js';\n\nconst ENDPOINT = 'https://www.google-analytics.com/mp/collect';\n\n/** Credentials for the GA4 Measurement Protocol. */\nexport interface Ga4ServerCredentials {\n /** The `G-` id of the data stream. */\n measurementId: string;\n /** Created under Admin → Data Streams → Measurement Protocol. */\n apiSecret: string;\n}\n\n/** Optional configuration for the GA4 server provider. */\nexport interface Ga4ServerConfig {\n /** How this property spells each canonical event. Rarely needed. */\n events?: Record<string, EventBinding<string>>;\n /**\n * Sends `user_data` built from `user` (email, phone, first and last name),\n * normalised and SHA-256 hashed the way Google documents. Off by default:\n * it starts a new flow of personal data to Google, and the property needs\n * user-provided data collection turned on in GA4's admin.\n *\n * There is no consent check in it. To withhold it, pass `user: { userId }`.\n */\n userProvidedData?: boolean;\n}\n\n/**\n * The visitor cookie holds `GA1.<depth>.<client_id>`, and the client id\n * itself contains a dot, so the identifier is the last two segments, which\n * is what survives Google's varying prefix depth.\n *\n * The value must be the cookie as the browser wrote it. A host passing a\n * parsed map passes cookie values, not values it took apart itself.\n */\nfunction visitorId(cookies: Record<string, string>): string | undefined {\n const raw = cookies._ga;\n if (!raw) return undefined;\n\n const parts = raw.split('.');\n if (parts.length < 4) return undefined;\n\n return parts.slice(-2).join('.');\n}\n\n/**\n * The per-container cookie carries the session, in one of two layouts.\n *\n * Verified against Google's live tag on 2026-07-31 rather than assumed:\n *\n * - `GS2.1.s1785511401$o1$g0$t1785511401$j60$l0$h0`, the current one. The\n * third dot-segment packs several fields joined by `$`, and the session is\n * the first of them, prefixed with `s`.\n * - `GS1.1.1785511401.1.1.…`, the older one, where the third dot-segment is\n * the session on its own.\n *\n * Reading the third segment blindly works for the old layout and returns the\n * whole `s…$o1$g0$…` blob for the new one, which the vendor would accept and\n * silently fail to match. Hence the two shapes, and hence `undefined` rather\n * than a guess when neither fits.\n */\nfunction sessionId(cookies: Record<string, string>, measurementId: string): string | undefined {\n // Scoped to *this* property's cookie. A page with two GA4 properties has two\n // `_ga_*` cookies, and taking whichever came first would attach another\n // property's session to our event, which Google accepts and never matches.\n const value = cookies[`_ga_${measurementId.replace(/^G-/, '')}`];\n const segment = value?.split('.')[2];\n if (!segment) return undefined;\n\n const packed = /^s(\\d+)/.exec(segment);\n if (packed) return packed[1];\n\n // The older layout: a bare session id, nothing packed around it.\n return /^\\d+$/.test(segment) ? segment : undefined;\n}\n\n/**\n * The Measurement Protocol documents exactly two consent fields, and spells\n * their values in upper case where the browser spells them in lower, one of\n * the small asymmetries an adapter absorbs so nobody else has to.\n */\nfunction consentBody(state: ConsentState | undefined): Record<string, string> | undefined {\n if (!state) return undefined;\n\n const body: Record<string, string> = {};\n if (state.ad_user_data) body.ad_user_data = state.ad_user_data.toUpperCase();\n if (state.ad_personalization) body.ad_personalization = state.ad_personalization.toUpperCase();\n\n return Object.keys(body).length > 0 ? body : undefined;\n}\n\n/**\n * The Measurement Protocol's documented limits: \"User property names must be\n * 24 characters or fewer\", \"User property values must be 36 characters or\n * fewer\", and at most 25 user properties.\n */\nconst MAX_PROPERTY_NAME = 24;\nconst MAX_PROPERTY_VALUE = 36;\nconst MAX_PROPERTIES = 25;\n\n/**\n * Traits as GA4 user properties. What GA4 cannot hold is left out and\n * reported by name, never replaced: a clear or a truncation would be a value\n * the host did not send. `null` is left out too, because clearing through\n * this protocol is not verified yet (ADR-0014).\n */\nfunction userProperties(\n traits: Record<string, unknown> | undefined,\n traitsAt: number | undefined,\n report: (message: string) => void,\n): Record<string, Record<string, unknown>> | undefined {\n if (!traits) return undefined;\n\n const built: Record<string, Record<string, unknown>> = {};\n const unclearable: string[] = [];\n const tooLong: string[] = [];\n\n for (const [name, value] of Object.entries(traits)) {\n if (value === null) {\n unclearable.push(name);\n } else if (\n name.length > MAX_PROPERTY_NAME ||\n (typeof value === 'string' && value.length > MAX_PROPERTY_VALUE)\n ) {\n tooLong.push(name);\n } else {\n // Each property is an object with a `value` key here, where the\n // browser takes the bare value. Sending the bare value is rejected\n // by Google's validation endpoint and **accepted with 204 by the\n // collection endpoint**, which then drops it, verified on\n // 2026-07-31. One of several places where this vendor fails by\n // accepting.\n built[name] = {\n value,\n ...(traitsAt === undefined ? {} : { timestamp_micros: traitsAt * 1000 }),\n };\n }\n }\n\n if (unclearable.length > 0) {\n report(\n `user properties not cleared, because clearing one through the Measurement Protocol is not verified: ${unclearable.join(', ')}`,\n );\n }\n if (tooLong.length > 0) {\n report(\n `user properties left out, over GA4's ${MAX_PROPERTY_NAME}-character name or ${MAX_PROPERTY_VALUE}-character value limit: ${tooLong.join(', ')}`,\n );\n }\n if (Object.keys(built).length > MAX_PROPERTIES) {\n // Not trimmed: which properties matter is the host's choice, not ours.\n report(\n `more than ${MAX_PROPERTIES} user properties, GA4's limit: all were sent, and Google decides which it keeps`,\n );\n }\n\n return Object.keys(built).length > 0 ? built : undefined;\n}\n\nasync function sha256(value: string): Promise<string> {\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value));\n\n return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('');\n}\n\n/**\n * Google's documented normalisation for user-provided data, which differs\n * from Meta's and LinkedIn's: Gmail addresses lose the dots before `@`, and\n * phones keep a leading `+`.\n */\nfunction normaliseEmail(value: string): string {\n const email = value.replace(/\\s/g, '').toLowerCase();\n const [local, domain] = email.split('@');\n if (local === undefined || domain === undefined) return email;\n\n return domain === 'gmail.com' || domain === 'googlemail.com'\n ? `${local.replace(/\\./g, '')}@${domain}`\n : email;\n}\n\nfunction normalisePhone(value: string): string {\n const digits = value.replace(/\\D/g, '');\n return digits ? `+${digits}` : '';\n}\n\nfunction normaliseName(value: string): string {\n return value\n .replace(/[^\\p{L}\\s]/gu, '')\n .toLowerCase()\n .trim();\n}\n\nasync function userData(user: UserData | undefined): Promise<Record<string, unknown> | undefined> {\n if (!user) return undefined;\n\n const built: Record<string, unknown> = {};\n const hashed = async (raw: string | undefined, normalise: (value: string) => string) => {\n const normalised = raw === undefined ? '' : normalise(raw);\n // An empty value still hashes to a valid digest that matches nobody.\n return normalised ? sha256(normalised) : undefined;\n };\n\n const email = await hashed(user.email, normaliseEmail);\n if (email) built.sha256_email_address = [email];\n\n const phone = await hashed(user.phone, normalisePhone);\n if (phone) built.sha256_phone_number = [phone];\n\n const firstName = await hashed(user.firstName, normaliseName);\n const lastName = await hashed(user.lastName, normaliseName);\n if (firstName || lastName) {\n built.address = [\n {\n ...(firstName ? { sha256_first_name: firstName } : {}),\n ...(lastName ? { sha256_last_name: lastName } : {}),\n },\n ];\n }\n\n return Object.keys(built).length > 0 ? built : undefined;\n}\n\n/**\n * Google Analytics 4 on the server, through the Measurement Protocol.\n *\n * **`identify` sends user properties with no event**, which Google\n * documents. With no user property to send it sends nothing, because each\n * `track` already carries `user_id`.\n *\n * @param credentials - The measurement id and API secret.\n * @param config - Rarely needed event name overrides, and the opt-in for\n * user-provided data.\n * @returns A provider for `createTracking` from `tracklane/server`.\n *\n * @example\n * ```ts\n * import { createTracking, ga4 } from 'tracklane/server';\n *\n * const { track } = createTracking({\n * providers: [ga4({ measurementId: 'G-XXXXXXX', apiSecret: process.env.GA4_SECRET! })],\n * });\n *\n * await track('purchase', order, { cookies: request.headers.get('cookie') });\n * ```\n */\nexport function ga4(\n credentials: Ga4ServerCredentials,\n config: Ga4ServerConfig = {},\n): ServerProvider {\n const { measurementId, apiSecret } = credentials;\n\n const clientIdOf = (context: ResolvedContext): string => {\n const clientId = visitorId(context.cookies);\n if (!clientId) {\n // Not withholding on policy grounds: the protocol requires this and\n // it cannot be invented. The endpoint answers success even to\n // invalid payloads, so sending anyway would look like it worked.\n // Naming the vendor's own field, so that searching Google's docs\n // for the error leads somewhere.\n throw new Error(\n \"ga4: cannot build client_id, no _ga cookie among the cookies passed in context. Ensure GA4's browser tag has set it first.\",\n );\n }\n return clientId;\n };\n\n // What every request carries about the person, with or without an event.\n const person = async (\n context: ResolvedContext,\n report: (message: string) => void,\n ): Promise<Record<string, unknown>> => {\n const body: Record<string, unknown> = {\n client_id: clientIdOf(context),\n timestamp_micros: context.timestamp * 1000,\n };\n\n if (context.user?.userId !== undefined) body.user_id = context.user.userId;\n\n const properties = userProperties(context.traits, context.traitsAt, report);\n if (properties) body.user_properties = properties;\n\n if (config.userProvidedData) {\n const data = await userData(context.user);\n if (data) body.user_data = data;\n }\n\n const consent = consentBody(context.consent);\n if (consent) body.consent = consent;\n\n return body;\n };\n\n const send = async (body: Record<string, unknown>, signal?: AbortSignal): Promise<void> => {\n // Credentials go in the query because the protocol offers no\n // alternative. Query strings land in proxy and APM logs; that\n // exposure is Google's design, not a choice made here.\n const url = `${ENDPOINT}?measurement_id=${encodeURIComponent(measurementId)}&api_secret=${encodeURIComponent(apiSecret)}`;\n\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n ...(signal ? { signal } : {}),\n });\n\n if (!response.ok) {\n // The message stays free of the payload; the body rides on the\n // error for a host that asks (ADR-0013). A body that will not read\n // must not replace the real failure with a different one.\n const text = await response.text().catch(() => '');\n throw new VendorResponseError(`ga4: the Measurement Protocol answered ${response.status}`, {\n provider: 'ga4',\n status: response.status,\n body: text,\n });\n }\n };\n\n return {\n name: 'ga4',\n default: 'passthrough',\n ...(config.events ? { events: config.events } : {}),\n\n async track(\n name: string,\n data: EventData,\n context: ResolvedContext,\n report: (message: string) => void,\n ): Promise<void> {\n const body = await person(context, report);\n\n const session = sessionId(context.cookies, measurementId);\n if (!session) {\n report(\n 'no session cookie (_ga_<container>): Google will accept this event and it will not appear in any report',\n );\n }\n\n const params: Record<string, unknown> = { ...data };\n if (session) {\n params.session_id = session;\n // Google drops sessions with no engagement from its reports, so the\n // event would arrive and still be invisible without this. A value\n // the host measured itself wins over the placeholder.\n params.engagement_time_msec ??= 1;\n }\n\n // The dedup id maps nowhere: GA4 documents no browser-to-server\n // deduplication. `transaction_id` (business data) travels in params\n // and is the duplicate control this vendor does have.\n await send({ ...body, events: [{ name, params }] }, context.signal);\n },\n\n async identify(context: ResolvedContext, report: (message: string) => void): Promise<void> {\n if (context.user?.userId === undefined) throw new Error('ga4: identify needs user.userId');\n\n const body = await person(context, report);\n // A request with neither events nor user properties is not a case\n // Google documents, and it would teach GA4 nothing: `user_id` already\n // travels on every `track`.\n if (!body.user_properties) return;\n\n await send(body, context.signal);\n },\n };\n}\n","import type { EventBinding, EventData, ResolvedContext, ServerProvider } from '../index.js';\nimport { VendorResponseError } from '../index.js';\n\n/** Credentials for LinkedIn's Conversions API. */\nexport interface LinkedinServerCredentials {\n /** An access token authorized for the conversion's ad account. */\n accessToken: string;\n}\n\n/** Configuration for the LinkedIn Conversions API provider. */\nexport interface LinkedinServerConfig {\n /** Conversion IDs minted in Campaign Manager. `null` explicitly suppresses an event. */\n events: Record<string, EventBinding<string>>;\n /** LinkedIn's Marketing API version, in YYYYMM form. Defaults to `202609`. */\n apiVersion?: string;\n}\n\nasync function sha256(value: string): Promise<string> {\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value));\n return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('');\n}\n\nfunction ipv4(value: string): boolean {\n const parts = value.split('.');\n return parts.length === 4 && parts.every((part) => /^\\d{1,3}$/.test(part) && Number(part) <= 255);\n}\n\nasync function userData(context: ResolvedContext): Promise<Record<string, unknown>> {\n const user = context.user ?? {};\n // Required even when matching uses only userInfo or externalIds.\n const userIds: { idType: string; idValue: string }[] = [];\n const built: Record<string, unknown> = { userIds };\n const email = user.email?.replace(/\\s/g, '').toLowerCase();\n if (email) userIds.push({ idType: 'SHA256_EMAIL', idValue: await sha256(email) });\n const clickId = context.cookies.li_fat_id;\n if (clickId) {\n // The vendor's documented cookie reader decodes URI encoding. A malformed\n // encoding must not invalidate a conversion that has another identifier.\n try {\n userIds.push({\n idType: 'LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID',\n idValue: decodeURIComponent(clickId),\n });\n } catch {\n /* malformed cookie: no identifier can be recovered */\n }\n }\n // LinkedIn supports only IPv4. Never substitute or manufacture an address.\n if (context.ip && ipv4(context.ip)) {\n userIds.push({ idType: 'PLAINTEXT_IP_ADDRESS', idValue: context.ip });\n }\n if (user.firstName && user.lastName) {\n built.userInfo = { firstName: user.firstName, lastName: user.lastName };\n }\n if (user.userId) built.externalIds = [user.userId];\n if (userIds.length === 0 && built.userInfo === undefined && built.externalIds === undefined) {\n throw new Error(\n 'linkedin: the Conversions API requires a user identifier, full name, or external ID',\n );\n }\n return built;\n}\n\n/**\n * Sends event-specific conversions to LinkedIn's Conversions API.\n * Value and currency become conversionValue; items, transaction_id, custom\n * properties, traits and consent have no documented field on this endpoint.\n * The host's deduplication ID becomes eventId, without generating one.\n *\n * @param credentials - The ad account's Conversions API access token.\n * @param config - Canonical event to conversion ID map and API version.\n * @returns A server provider for `createTracking`.\n * @example\n * ```ts\n * import { createTracking, linkedin } from 'tracklane/server';\n * const { track } = createTracking({\n * providers: [linkedin({ accessToken }, { events: { purchase: '12345' } })],\n * });\n * await track('purchase', { value: 15, currency: 'BRL' }, {\n * user: { email: 'buyer@example.com' }, dedupId: 'order-1',\n * });\n * ```\n */\nexport function linkedin(\n credentials: LinkedinServerCredentials,\n config: LinkedinServerConfig,\n): ServerProvider {\n const { apiVersion = '202609' } = config;\n return {\n name: 'linkedin',\n default: 'ignore',\n events: config.events,\n async track(\n name: string,\n data: EventData,\n context: ResolvedContext,\n report: (message: string) => void,\n ): Promise<void> {\n if (!/^\\d+$/.test(name) || BigInt(name) <= 0n) {\n throw new Error(\n 'linkedin: conversion must identify a positive conversion ID from Campaign Manager',\n );\n }\n const event: Record<string, unknown> = {\n conversion: `urn:lla:llaPartnerConversion:${name}`,\n conversionHappenedAt: context.timestamp,\n user: await userData(context),\n };\n if (data.value !== undefined && data.currency !== undefined) {\n event.conversionValue = { currencyCode: data.currency, amount: String(data.value) };\n }\n if (context.dedupId !== undefined) event.eventId = context.dedupId;\n const response = await fetch('https://api.linkedin.com/rest/conversionEvents', {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${credentials.accessToken}`,\n 'Content-Type': 'application/json',\n 'Linkedin-Version': apiVersion,\n 'X-Restli-Protocol-Version': '2.0.0',\n },\n body: JSON.stringify(event),\n ...(context.signal ? { signal: context.signal } : {}),\n });\n if (!response.ok) {\n const body = await response.text().catch(() => '');\n throw new VendorResponseError(`linkedin: the Conversions API answered ${response.status}`, {\n provider: 'linkedin',\n status: response.status,\n body,\n });\n }\n if (data.value !== undefined && data.currency === undefined) {\n report(\n 'conversionValue omitted: a monetary value requires currency; the conversion rule value applies',\n );\n }\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 ActionSource,\n EventBinding,\n EventData,\n EventItem,\n ResolvedContext,\n ServerProvider,\n} from '../index.js';\nimport { VendorResponseError } from '../index.js';\n\nconst GRAPH = 'https://graph.facebook.com';\n\n/**\n * Pinned on purpose. A Graph URL with no version resolves to the *oldest*\n * version still available, which silently changes what the endpoint accepts\n * the day Meta drops it.\n */\nconst DEFAULT_API_VERSION = 'v25.0';\n\n/**\n * How Meta spells the canonical vocabulary, which is GA4's. Kept in step with\n * `meta.browser.ts` by hand: the two halves are two libraries, and neither\n * imports the other.\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/** Credentials for Meta's Conversions API. */\nexport interface MetaServerCredentials {\n /** The pixel (dataset) id events are posted to. */\n pixelId: string;\n /** A system user or page access token with `ads_management`. It is a credential. */\n accessToken: string;\n}\n\n/** Optional configuration for the Meta server provider. */\nexport interface MetaServerConfig {\n /** How this pixel spells each canonical event, merged over the built-in map. */\n events?: Record<string, EventBinding<string>>;\n /**\n * Where conversions happen by default. Meta requires `action_source` on\n * every event, so there is always a value; `context.source` overrides it\n * per call, which is what a webhook and a checkout need.\n */\n actionSource?: ActionSource;\n /** The Graph API version in the URL. Defaults to a pinned one. */\n apiVersion?: string;\n /**\n * Routes events to the Test Events tab in Events Manager instead of the\n * live dataset. **Testing only**: Meta says to remove it before\n * production, and it is the only instrument that shows what a payload\n * actually became.\n */\n testEventCode?: string;\n /**\n * Meta's Limited Data Use marking, forwarded verbatim: `['LDU']` to enable\n * it, `[]` to disable it.\n *\n * This is a processing marking that some US state laws require, not a\n * consent declaration, which is why it is configuration on this factory and\n * not a reading of `context.consent`. The browser half has no equivalent:\n * `fbq('dataProcessingOptions', …)` is documented as running before\n * `fbq('init')`, and the snippet is the host's.\n */\n dataProcessingOptions?: readonly string[];\n /** `0` lets Meta geolocate. Meaningless without `dataProcessingOptions`. */\n dataProcessingOptionsCountry?: number;\n /** `0` lets Meta geolocate. Meaningless without `dataProcessingOptions`. */\n dataProcessingOptionsState?: number;\n}\n\n/**\n * Web Crypto rather than `node:crypto`, so the built server bundle stays\n * platform-neutral: this adapter is called from Cloudflare Workers, Deno and\n * Vercel's edge runtime as often as from a Node process, and a `node:` import\n * would rule those out for no gain.\n */\nasync function sha256(value: string): Promise<string> {\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value));\n\n return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('');\n}\n\n/**\n * **Why the adapter hashes and does not ask the host to.**\n *\n * This looks like the library transforming data, and the cutting rule forbids\n * that. It is not. Meta does not define a field called \"email\" that happens\n * to want a hash: it defines `em` *as* the SHA-256 of the trimmed, lowercased\n * address, the same way it defines `event_time` as seconds rather than\n * milliseconds. Writing this line by hand you write\n * `sha256(email.trim().toLowerCase())`, and the adapter replicates exactly\n * that. Normalisation lives with the field it belongs to.\n *\n * The alternative is worse and is where integrations demonstrably go wrong:\n * three vendors want SHA-256 under three different normalisation rules, so a\n * host hashing on its own side has to hash the same address three ways and\n * discovers a mistake only as a quietly poor match rate.\n */\nconst normaliseEmail = (value: string): string => value.trim().toLowerCase();\n\n/**\n * \"Remove symbols, letters, and any leading zeros.\" The country code is not\n * added here: Meta wants one and the library does not know it, and guessing a\n * country onto a phone number invents an identifier for a different person.\n */\nconst normalisePhone = (value: string): string => value.replace(/\\D/g, '').replace(/^0+/, '');\n\n/** \"Lowercase only with no punctuation.\" Spaces and accents survive; Meta reads UTF-8. */\nconst normaliseName = (value: string): string =>\n value\n .trim()\n .toLowerCase()\n .replace(/[\\p{P}\\p{S}]/gu, '');\n\n/**\n * GA4's `items` become Meta's `contents`, a closed shape: `id`, `quantity`,\n * `item_price`, `delivery_category`. The rest of a GA4 item is dropped rather\n * than forwarded, because a key Meta does not read inside `contents` looks\n * mapped and arrives nowhere.\n */\nfunction contents(items: readonly EventItem[]): Record<string, unknown>[] | undefined {\n // All or none: a partial list would silently describe only part of the order.\n // Check whitespace without rewriting the identifier the host supplied.\n if (items.some((item) => typeof item.item_id !== 'string' || !item.item_id.trim()))\n return undefined;\n\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. Everything\n * the host named itself passes through, because a business measures things\n * this vocabulary does not name.\n *\n * `transaction_id` becomes `order_id`, which this surface documents and the\n * pixel does not. It is one of the asymmetries between Meta's two halves an\n * adapter absorbs so nobody else has to.\n */\nfunction customData(data: EventData): Record<string, unknown> {\n const { items, transaction_id: transactionId, ...rest } = data;\n const custom: Record<string, unknown> = { ...rest };\n\n if (transactionId !== undefined) custom.order_id = transactionId;\n if (items) {\n const mapped = contents(items);\n if (mapped !== undefined) custom.contents = mapped;\n else delete custom.contents;\n }\n\n return custom;\n}\n\n/**\n * The heaviest surface of the five vendors: four hashed identifiers, four raw\n * ones, and two cookies forwarded exactly as the pixel wrote them.\n *\n * Built in a fixed order, and hashed one after another rather than in\n * parallel, so that the same input produces the same JSON on every run. Two\n * identical events differing only in key order would be the library inventing\n * something, which is precisely what the conformance suite looks for.\n */\nasync function userData(context: ResolvedContext): Promise<Record<string, string>> {\n const user = context.user ?? {};\n const built: Record<string, string> = {};\n\n const hashed: readonly [string, string | undefined, (value: string) => string][] = [\n ['em', user.email, normaliseEmail],\n ['ph', user.phone, normalisePhone],\n ['fn', user.firstName, normaliseName],\n ['ln', user.lastName, normaliseName],\n ];\n\n for (const [field, raw, normalise] of hashed) {\n if (raw === undefined) continue;\n\n const normalised = normalise(raw);\n // An empty value still hashes to a perfectly valid digest, one that\n // matches nobody and counts as an identifier towards the minimum below.\n if (!normalised) continue;\n\n built[field] = await sha256(normalised);\n }\n\n // Raw by Meta's own instruction. `external_id` is the one it merely\n // *recommends* hashing, and it stays raw here so the value Meta receives is\n // the value the host holds. A host that also puts an external id in its\n // `fbq('init')` should know the pixel hashes what it finds there, so the two\n // surfaces match only if both sides send the same form: pre-hash on both,\n // or on neither. Meta's pages do not state which side should give way.\n if (user.userId !== undefined) built.external_id = user.userId;\n if (context.ip !== undefined) built.client_ip_address = context.ip;\n if (context.userAgent !== undefined) built.client_user_agent = context.userAgent;\n\n // Meta forwards these as-is; they are its own cookies in its own format.\n // Note what is *not* here: an `fbc` synthesised from an `fbclid` in the URL.\n // Meta documents that construction, and it needs a click timestamp the\n // library does not have, so building one would be inventing an identifier.\n const fbp = context.cookies._fbp;\n if (fbp) built.fbp = fbp;\n\n const fbc = context.cookies._fbc;\n if (fbc) built.fbc = fbc;\n\n return built;\n}\n\n/**\n * Meta's Conversions API.\n *\n * If any item lacks a nonempty `item_id`, the complete `contents` list is\n * omitted while totals and order metadata remain unchanged. This avoids\n * presenting a partial order or inventing product identifiers. The error\n * channel receives one payload-free warning after HTTP acceptance.\n *\n * @param credentials - The pixel id and an access token.\n * @param config - Event overrides, the default action source, and Meta's own\n * levers: the API version, a test event code, and Limited Data Use.\n * @returns A provider for `createTracking` from `tracklane/server`.\n *\n * @example\n * ```ts\n * import { createTracking, meta } from 'tracklane/server';\n *\n * const { track } = createTracking({\n * providers: [meta({ pixelId: '1234567890', accessToken: process.env.META_TOKEN! })],\n * });\n *\n * await track('purchase', order, {\n * user: { email: order.email },\n * cookies: request.headers.get('cookie'),\n * dedupId: order.id,\n * ip,\n * userAgent,\n * });\n * ```\n */\nexport function meta(\n credentials: MetaServerCredentials,\n config: MetaServerConfig = {},\n): ServerProvider {\n const { pixelId, accessToken } = credentials;\n const { actionSource = 'website', apiVersion = DEFAULT_API_VERSION, testEventCode } = config;\n\n return {\n name: 'meta',\n default: 'passthrough',\n events: { ...CANONICAL_EVENTS, ...config.events },\n\n async track(\n name: string,\n data: EventData,\n context: ResolvedContext,\n report: (message: string) => void,\n ): Promise<void> {\n const user = await userData(context);\n\n if (Object.keys(user).length === 0) {\n // Not withholding on policy grounds: Meta requires at least one\n // `user_data` parameter and none can be invented. Its own page says\n // \"at least one of the following\" over the whole list, so a bare\n // `fbp` qualifies; whether matching is any good with a non-contact\n // identifier alone is Meta's business, not this adapter's to judge.\n // Naming the vendor's own field, so searching for the error leads\n // somewhere.\n throw new Error(\n 'meta: cannot build user_data, the Conversions API requires at least one identifier. Pass context.user, context.ip with context.userAgent, or the _fbp/_fbc cookies.',\n );\n }\n\n const event: Record<string, unknown> = {\n event_name: name,\n // Seconds, where the context carries milliseconds.\n event_time: Math.floor(context.timestamp / 1000),\n // Required on every event, which is why the factory carries a default\n // rather than letting the request fail.\n action_source: context.source ?? actionSource,\n user_data: user,\n };\n\n // Meta's key is `event_id` here and `eventID` on the pixel, matched\n // together with `event_name`. Never produced by the library.\n if (context.dedupId !== undefined) event.event_id = context.dedupId;\n if (context.url !== undefined) event.event_source_url = context.url;\n\n const custom = customData(data);\n if (Object.keys(custom).length > 0) event.custom_data = custom;\n\n if (config.dataProcessingOptions) {\n // Meta lists these among its *server event* parameters, alongside\n // `event_name` and `user_data`, which is why they sit inside the\n // event. No example on its pages states the level outright.\n event.data_processing_options = config.dataProcessingOptions;\n\n if (config.dataProcessingOptionsCountry !== undefined) {\n event.data_processing_options_country = config.dataProcessingOptionsCountry;\n }\n if (config.dataProcessingOptionsState !== undefined) {\n event.data_processing_options_state = config.dataProcessingOptionsState;\n }\n }\n\n // `context.traits` and `context.consent` map to nothing. Meta has no\n // person-properties slot on this endpoint, and its consent surface is a\n // browser command; the processing marking above is a different concept\n // wearing similar words.\n\n // Form-encoded, which is the shape Meta's own examples post, and it\n // keeps the token out of the query string. Unlike GA4, this vendor\n // offers a body slot for it, so query strings in proxy and APM logs are\n // avoidable here.\n const body = new URLSearchParams({\n data: JSON.stringify([event]),\n access_token: accessToken,\n });\n\n if (testEventCode) body.set('test_event_code', testEventCode);\n\n const response = await fetch(`${GRAPH}/${apiVersion}/${encodeURIComponent(pixelId)}/events`, {\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded' },\n body,\n ...(context.signal ? { signal: context.signal } : {}),\n });\n\n if (!response.ok) {\n // The message stays free of the payload; the body rides on the\n // error for a host that asks (ADR-0013). A body that will not read\n // must not replace the real failure with a different one.\n const body = await response.text().catch(() => '');\n throw new VendorResponseError(`meta: the Conversions API answered ${response.status}`, {\n provider: 'meta',\n status: response.status,\n body,\n });\n }\n\n if (data.items && custom.contents === undefined) {\n report('contents omitted: every item needs a nonempty item_id; totals were preserved');\n }\n\n // A body that will not parse must not turn a delivered conversion into\n // an error, which would tell a host's retry logic to send it again.\n const result = (await response.json().catch(() => null)) as {\n messages?: unknown[];\n fbtrace_id?: string;\n } | null;\n\n const messages = result?.messages;\n if (Array.isArray(messages) && messages.length > 0) {\n // A caveat about a send that already happened, so it reports rather\n // than throws. The count and the trace id only: Meta's warnings can\n // quote the parameter that upset it, and event contents never reach a\n // host's logs through this library.\n report(\n `accepted with ${messages.length} warning(s) from the Conversions API; read them under fbtrace_id ${result?.fbtrace_id ?? 'unknown'} in Events Manager`,\n );\n }\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 { EventBinding, EventData, ResolvedContext, ServerProvider } from '../index.js';\nimport { VendorResponseError } from '../index.js';\n\nconst DEFAULT_HOST = 'https://us.i.posthog.com';\n\n/** Credentials for PostHog's Capture API. */\nexport interface PosthogServerCredentials {\n /** The project API key, the same one the browser SDK is initialised with (`phc_...`). */\n apiKey: string;\n /**\n * Where this project ingests events, when it is not PostHog's default US\n * Cloud instance — `https://eu.i.posthog.com` for the EU Cloud, or a\n * self-hosted deployment's own origin. Defaults to `https://us.i.posthog.com`.\n */\n host?: string;\n}\n\n/** Optional configuration for the PostHog server provider. */\nexport interface PosthogServerConfig {\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/**\n * The distinct_id lives inside a cookie posthog-js writes by default, named\n * after this project's own api key: `ph_<api_key>_posthog`. Its value is\n * `encodeURIComponent(JSON.stringify({ distinct_id, ... }))`, so it has to be\n * decoded before it parses as JSON.\n *\n * Verified against a live project rather than read off the SDK's source: the\n * cookie was percent-encoded, held `distinct_id` alongside `$device_id` and\n * `$user_state`, and the id parsed out of it matched what the page's own\n * `posthog.get_distinct_id()` returned. A server event sent with it landed on\n * the same person the browser had created, which is the whole point.\n *\n * This is only the anonymous fallback. It is not read when the caller already\n * knows who the visitor is; see the comment in `track` for why.\n */\nfunction anonymousId(cookies: Record<string, string>, apiKey: string): string | undefined {\n const raw = cookies[`ph_${apiKey}_posthog`];\n if (!raw) return undefined;\n\n try {\n const parsed = JSON.parse(decodeURIComponent(raw)) as { distinct_id?: unknown };\n return typeof parsed.distinct_id === 'string' ? parsed.distinct_id : undefined;\n } catch {\n // A cookie that fails to decode or parse is not this vendor's cookie, or\n // not this shape of it. Either way, nothing to read.\n return undefined;\n }\n}\n\n/**\n * PostHog deduplicates on `uuid`, and the field holds a UUID rather than any\n * string: an invalid one is documented as dropped on ingestion, and has been\n * reported answering 400 instead. Since `dedupId` is whatever the host chose\n * — Meta's `event_id` takes anything, and this library's own example is an\n * order id — forwarding it unchecked would turn a duplicated purchase into no\n * purchase at all.\n *\n * Accepts any RFC 4122 layout rather than v4 alone, because v7 is what a\n * system minting ids today tends to produce.\n */\nconst UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n\n/**\n * PostHog documents that it \"shortens any `distinct_id` longer than 200\n * characters\" and ingests the event under the shortened id. Sent as given,\n * because refusing would drop an event the vendor accepts; reported, because\n * two ids sharing a prefix become one person and only the host can fix that.\n */\nconst MAX_DISTINCT_ID = 200;\n\nfunction checkLength(id: string, report: (message: string) => void): void {\n if (id.length > MAX_DISTINCT_ID) {\n report(\n `distinct_id is longer than ${MAX_DISTINCT_ID} characters: PostHog truncates it, so two ids sharing that prefix become one person`,\n );\n }\n}\n\n/**\n * Traits with a value go to `$set`; a trait set to `null` means \"no longer\n * known\", which PostHog spells as a name in `$unset`.\n */\nfunction personProperties(traits: Record<string, unknown> | undefined): {\n $set?: Record<string, unknown>;\n $unset?: string[];\n} {\n if (!traits) return {};\n\n const set: Record<string, unknown> = {};\n const unset: string[] = [];\n for (const [name, value] of Object.entries(traits)) {\n if (value === null) unset.push(name);\n else set[name] = value;\n }\n\n return {\n ...(Object.keys(set).length > 0 ? { $set: set } : {}),\n ...(unset.length > 0 ? { $unset: unset } : {}),\n };\n}\n\n/**\n * PostHog on the server, through the Capture API.\n *\n * **Deduplicating a retried webhook takes two things, not one.** `dedupId`\n * becomes PostHog's `uuid`, and PostHog collapses events sharing `uuid`, event\n * name, timestamp and `distinct_id` — so a `timestamp` left to default to the\n * clock differs on the retry and nothing is deduplicated. Pin it to a value\n * derived from the order, as below. The `dedupId` itself must be a UUID:\n * anything else is reported through `onError` and sent without the field,\n * because PostHog rejects other shapes rather than ignoring them.\n *\n * Deduplication is also eventual, happening during a background merge, so both\n * rows are visible for a while and a test that counts immediately will fail.\n *\n * **`identify` sends one `$identify`**, with `$set` and `$unset` from the\n * traits. It merges a visitor into the user only when the context carries\n * `anonymousId`, never from the cookie: a merge is irreversible, and PostHog\n * reports a refused one only as an ingestion warning in its own interface.\n * Identified events are billed with person processing.\n *\n * @param credentials - The project API key, and the ingestion host for\n * anything other than PostHog's default US Cloud instance.\n * @param config - Rarely needed event name overrides.\n * @returns A provider for `createTracking` from `tracklane/server`.\n *\n * @example\n * ```ts\n * import { createTracking, posthog } from 'tracklane/server';\n *\n * const { track } = createTracking({\n * providers: [posthog({ apiKey: process.env.POSTHOG_API_KEY! })],\n * });\n *\n * // Both fields come from the order, so the payment provider's second\n * // delivery of the same webhook produces the same event.\n * await track('purchase', order, {\n * user: { userId: order.userId },\n * cookies: request.headers.get('cookie'),\n * dedupId: order.eventId, // a UUID your checkout minted with the order\n * timestamp: order.paidAt,\n * });\n * ```\n */\nexport function posthog(\n credentials: PosthogServerCredentials,\n config: PosthogServerConfig = {},\n): ServerProvider {\n const { apiKey, host = DEFAULT_HOST } = credentials;\n\n const send = async (body: Record<string, unknown>, signal?: AbortSignal): Promise<void> => {\n const response = await fetch(`${host}/i/v0/e/`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ api_key: apiKey, ...body }),\n ...(signal ? { signal } : {}),\n });\n\n if (!response.ok) {\n // The message stays free of the payload; the body rides on the\n // error for a host that asks (ADR-0013). A body that will not read\n // must not replace the real failure with a different one.\n const text = await response.text().catch(() => '');\n throw new VendorResponseError(`posthog: the capture endpoint answered ${response.status}`, {\n provider: 'posthog',\n status: response.status,\n body: text,\n });\n }\n };\n\n return {\n name: 'posthog',\n // capture() accepts any string as an event name, so the canonical\n // vocabulary already is PostHog's, and every event passes through\n // unless the host says otherwise.\n default: 'passthrough',\n ...(config.events ? { events: config.events } : {}),\n\n async track(\n name: string,\n data: EventData,\n context: ResolvedContext,\n report: (message: string) => void,\n ): Promise<void> {\n // PostHog has one identity slot, distinct_id — not two the way GA4 has\n // a required client_id plus an optional user_id. PostHog's own\n // guidance is to send the same distinct_id the browser SDK is using\n // for that visitor, so a known application user id (the same value\n // handed to `identify()` on the browser) is used first, because it is\n // what the visitor becomes once they are known. The anonymous id\n // PostHog's own tag already wrote to its cookie is the fallback for a\n // visitor nobody has identified yet. Inventing a third value here\n // would orphan the event: PostHog would accept it and never connect it\n // to anything, on either side.\n // A host's own anonymous id comes before the cookie, because the host\n // said so explicitly and the cookie is only inferred.\n const id =\n context.user?.userId ?? context.anonymousId ?? anonymousId(context.cookies, apiKey);\n if (!id) {\n throw new Error(\n \"posthog: cannot resolve distinct_id — no user.userId or anonymousId in context and no ph_<api_key>_posthog cookie among the cookies passed in context. Pass user.userId once the visitor is known, or ensure PostHog's browser tag has set its cookie first.\",\n );\n }\n checkLength(id, report);\n\n // $set is the server-side slot for what is known *about* the person,\n // the same job `user_properties` does in GA4's Measurement Protocol\n // body — kept separate from the identifiers used to match someone.\n const properties: Record<string, unknown> = {\n ...data,\n ...personProperties(context.traits),\n };\n\n // The deduplication key is `uuid`, and it is the whole quartet of\n // `uuid`, event name, timestamp and distinct_id that PostHog collapses,\n // eventually, during a background merge. So this field alone does not\n // make a retry idempotent: the host has to pin `context.timestamp` to\n // something derived from the order rather than let it default to the\n // clock. That is documentation's job, not this adapter's.\n const dedup: { uuid?: string } = {};\n if (context.dedupId !== undefined) {\n if (UUID.test(context.dedupId)) {\n dedup.uuid = context.dedupId;\n } else {\n // Sent anyway, without the field. The alternative is the vendor\n // dropping the whole event over an id shape, which trades a visible\n // duplicate for an invisible absence.\n report(\n 'dedupId is not a UUID, so it was not forwarded as uuid: PostHog deduplicates on that field and rejects other shapes, so this event can duplicate on a retry',\n );\n }\n }\n\n const body = {\n event: name,\n distinct_id: id,\n ...dedup,\n // context.url, context.ip and context.userAgent are not forwarded\n // either. PostHog documents override properties for some of these\n // on other surfaces, but nothing found here confirms one for this\n // endpoint with enough certainty to translate rather than guess, so\n // they are left unmapped. context.consent and context.source are\n // Google's and Meta's vocabularies respectively, and this endpoint\n // documents a slot for neither.\n properties,\n timestamp: new Date(context.timestamp).toISOString(),\n };\n\n await send(body, context.signal);\n },\n\n async identify(context: ResolvedContext, report: (message: string) => void): Promise<void> {\n const id = context.user?.userId;\n if (!id) throw new Error('posthog: identify needs user.userId');\n checkLength(id, report);\n\n // The body posthog-node's own `identify` sends, plus the merge field\n // posthog-js sends. The merge is read from the host's explicit\n // anonymousId only, never from the cookie (ADR-0011, ADR-0014).\n await send(\n {\n event: '$identify',\n distinct_id: id,\n properties: {\n ...personProperties(context.traits),\n ...(context.anonymousId === undefined\n ? {}\n : { $anon_distinct_id: context.anonymousId }),\n },\n timestamp: new Date(context.timestamp).toISOString(),\n },\n context.signal,\n );\n },\n };\n}\n","import { assertUniqueProviders, makeReporter, messageOf, resolveEvent } from './dispatch.js';\n\nexport {\n type Ga4ServerConfig,\n type Ga4ServerCredentials,\n ga4,\n} from './providers/ga4.server.js';\nexport {\n type LinkedinServerConfig,\n type LinkedinServerCredentials,\n linkedin,\n} from './providers/linkedin.server.js';\nexport {\n type MetaServerConfig,\n type MetaServerCredentials,\n meta,\n} from './providers/meta.server.js';\nexport {\n type PosthogServerConfig,\n type PosthogServerCredentials,\n posthog,\n} from './providers/posthog.server.js';\n\nimport type {\n ActionSource,\n ConsentState,\n EventBinding,\n EventData,\n ProviderDefault,\n TrackingError,\n UserData,\n} from './vocabulary.js';\n\n/**\n * Everything a conversion API needs beyond the event itself: who the person\n * is, when it happened, and where it was observed.\n *\n * None of this is \"what happened\", which is why it does not travel in the\n * event data: putting it there would leak identity into the pass-through\n * event params of the vendors that forward the payload as-is.\n */\nexport interface EventContext {\n /** The person's identity, the server-side counterpart to `identify()` on the browser. */\n user?: UserData;\n /**\n * The request's raw `Cookie` header, or an already-parsed map.\n *\n * These are cookies the vendors' own browser tags set. This library never\n * writes one. On the server each adapter reads the cookies belonging to\n * its own vendor: Meta forwards its value as-is, Google's holds several\n * things at once and has to be opened. That knowledge lives in the\n * adapters; the core only splits `name=value; name=value` into pairs.\n *\n * **It splits, it does not interpret.** Values are handed over exactly as\n * they appear, with no percent-decoding. The cookies these vendors set do\n * not need it; a host reading some other cookie should decode it itself.\n */\n cookies?: string | Record<string, string> | null;\n /** Ties this send to a browser hit. Never invented by the library. */\n dedupId?: string;\n /**\n * When the conversion actually happened. Defaults to now.\n *\n * A webhook arrives after the fact and LinkedIn rejects conversions older\n * than 90 days, so reporting \"now\" for everything would quietly distort\n * attribution.\n */\n timestamp?: number | Date;\n /** Overrides the factory default. Meta requires it on every event. */\n source?: ActionSource;\n /** The page the conversion happened on. Meta's Conversions API reads it as `event_source_url`. */\n url?: string;\n /** The visitor's IP. Meta's Conversions API reads it as one of the identifiers in `user_data`. */\n ip?: string;\n /** The visitor's user agent. Meta's Conversions API reads it alongside `ip`. */\n userAgent?: string;\n /**\n * What is known *about* the person: GA4 user properties, PostHog `$set`.\n *\n * A trait set to `null` means \"no longer known\". PostHog receives it in\n * `$unset`. GA4 leaves it out and reports that it did, because clearing a\n * user property through the Measurement Protocol is not yet verified.\n */\n traits?: Record<string, unknown>;\n /**\n * When the traits were true, if not when the event happened. Same units as\n * `timestamp`. GA4 stamps each user property with it; vendors with no slot\n * ignore it.\n */\n traitsAt?: number | Date;\n /**\n * Your own stable id for a visitor nobody has identified yet. Never\n * generated by the library.\n *\n * PostHog uses it as `distinct_id` when there is no `user.userId`, ahead of\n * its cookie, and `identify` merges it into the user. It must be the id the\n * earlier PostHog events were captured under, or it merges nothing.\n */\n anonymousId?: string;\n /** Cancels the vendor requests. An aborted send is reported as an error. */\n signal?: AbortSignal;\n /**\n * Travels inside the event, for the vendors that document such a field.\n * Today that is GA4's Measurement Protocol, and it reads two of the\n * signals; the others have no slot and ignore it.\n */\n consent?: ConsentState;\n}\n\n/** The context an adapter receives, with the ambiguity resolved. */\nexport interface ResolvedContext extends Omit<EventContext, 'cookies' | 'timestamp' | 'traitsAt'> {\n /** The request's cookies, already split into a `name → value` map. */\n cookies: Record<string, string>;\n /** Epoch milliseconds. Adapters convert to their vendor's spelling. */\n timestamp: number;\n /** Epoch milliseconds, when the host gave one. */\n traitsAt?: number;\n}\n\n/**\n * The context {@link ServerTracking.identify} takes: the same one `track`\n * takes, with the person's id required.\n */\nexport interface IdentifyContext extends EventContext {\n /** Who the person is. `userId` is required: identifying nobody is not a call. */\n user: UserData & { userId: string };\n}\n\n/**\n * A destination for server events.\n *\n * `track` may throw, on a non-2xx or where a vendor documents a\n * precondition the event cannot meet. The dispatcher isolates and reports\n * it, so adapters stay honest and loud.\n *\n * `report` is for a caveat about a request that already succeeded. It does\n * not throw: by then the send happened, and throwing would tell a host's\n * retry logic to resend a delivered conversion.\n */\nexport interface ServerProvider<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 /** Sends one event to this vendor's conversion API. May throw, or call `report` for a caveat. */\n track(\n name: Target,\n data: EventData,\n context: ResolvedContext,\n report: (message: string) => void,\n ): Promise<void>;\n /**\n * Tells this vendor who the person is, with no event. Absent where the\n * vendor keeps no person of its own. May throw, or call `report` for a caveat.\n */\n identify?(context: ResolvedContext, report: (message: string) => void): Promise<void>;\n}\n\n/** Options accepted by {@link createTracking}. */\nexport interface ServerTrackingOptions {\n /** The vendors to send every event to. Registering the same `name` twice throws. */\n providers: readonly ServerProvider[];\n /**\n * The one diagnostic channel. Without it the library is silent, exactly as\n * a hand-written vendor call is.\n * Unmapped events on an `ignore` provider report warnings; explicit `null`\n * exclusions stay silent. Attach a reporter in development to catch omissions.\n */\n onError?: (error: TrackingError) => void;\n}\n\n/** What {@link createTracking} returns. */\nexport interface ServerTracking {\n /**\n * Sends one event to every configured conversion API.\n *\n * Resolves once every provider has settled and never rejects: one vendor\n * being down is not the caller's failure to handle. Awaiting is about the\n * request surviving a serverless runtime, not about delivery being\n * guaranteed.\n */\n track(name: string, data?: EventData, context?: EventContext): Promise<void>;\n /**\n * Tells every vendor that keeps a person who this one is, with no event:\n * PostHog's `$identify` and a GA4 request with user properties only.\n * Vendors that keep no person receive nothing.\n *\n * A merge happens only when `anonymousId` is given. PostHog merges are\n * irreversible, so pass it only when you know that visitor is this person.\n * Resolves once every provider has settled and never rejects.\n */\n identify(context: IdentifyContext): Promise<void>;\n}\n\n/** `name=value; name=value`, the shape of a `Cookie` header. */\nfunction parseCookies(\n cookies: string | Record<string, string> | null | undefined,\n): Record<string, string> {\n if (!cookies) return {};\n if (typeof cookies !== 'string') return cookies;\n\n const parsed: Record<string, string> = {};\n\n for (const pair of cookies.split(';')) {\n const separator = pair.indexOf('=');\n if (separator < 1) continue;\n\n const name = pair.slice(0, separator).trim();\n // Cookie values may contain `=`, so only the first one separates.\n if (name) parsed[name] = pair.slice(separator + 1).trim();\n }\n\n return parsed;\n}\n\nfunction toEpoch(value: number | Date): number {\n return value instanceof Date ? value.getTime() : value;\n}\n\nfunction resolveContext(context: EventContext): ResolvedContext {\n const { cookies, timestamp, traitsAt, ...rest } = context;\n\n return {\n ...rest,\n cookies: parseCookies(cookies),\n timestamp: timestamp === undefined ? Date.now() : toEpoch(timestamp),\n ...(traitsAt === undefined ? {} : { traitsAt: toEpoch(traitsAt) }),\n };\n}\n\n/**\n * Creates the server tracking surface.\n *\n * Nothing is held between calls: a server instance is shared by every\n * request, so a retained identity would attach one visitor to another\n * visitor's conversion. Identity travels with each call, `identify`\n * included, which is also what every conversion API expects.\n *\n * There is no `consent` call either. Consent commands are properties of a\n * vendor's on-page tag, and a server request has no tag. The one vendor that\n * accepts consent server-side reads it from the context.\n *\n * @param options - The providers to send to, and an optional error channel.\n * @returns The `track` and `identify` calls.\n *\n * @example\n * ```ts\n * import { createTracking, ga4 } from 'tracklane/server';\n *\n * const { track } = createTracking({\n * providers: [ga4({ measurementId: 'G-XXXXXXX', apiSecret })],\n * });\n *\n * await track('purchase', order, { cookies: request.headers.get('cookie') });\n * ```\n */\nexport function createTracking(options: ServerTrackingOptions): ServerTracking {\n const { providers } = options;\n assertUniqueProviders(providers);\n\n const report = makeReporter(options.onError);\n\n // One provider's send, isolated: a throw becomes an error report and a\n // caveat becomes a warning, and neither reaches the caller.\n const settle = async (\n provider: ServerProvider,\n event: string,\n send: (warn: (message: string) => void) => Promise<void>,\n ): Promise<void> => {\n try {\n await send((message) => {\n report({ provider: provider.name, event, severity: 'warning', message });\n });\n } catch (cause) {\n report({\n provider: provider.name,\n event,\n severity: 'error',\n message: messageOf(cause),\n cause,\n });\n }\n };\n\n return {\n async track(name, data = {}, context = {}) {\n const resolved = resolveContext(context);\n\n await Promise.all(\n providers.map(async (provider) => {\n const target = resolveEvent(provider, name, (message) => {\n report({ provider: provider.name, event: name, severity: 'warning', message });\n });\n if (target === undefined) return;\n\n await settle(provider, name, (warn) => provider.track(target, data, resolved, warn));\n }),\n );\n },\n\n async identify(context) {\n const resolved = resolveContext(context);\n\n await Promise.all(\n providers.map((provider) => {\n const identify = provider.identify?.bind(provider);\n if (!identify) return undefined;\n\n return settle(provider, 'identify', (warn) => identify(resolved, warn));\n }),\n );\n },\n };\n}\n"],"mappings":"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"}
package/dist/server.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { A as posthog, F as LinkedinServerCredentials, I as linkedin, L as Ga4ServerConfig, M as MetaServerCredentials, N as meta, O as PosthogServerConfig, P as LinkedinServerConfig, R as Ga4ServerCredentials, _ as ServerTracking, g as ServerProvider, h as ResolvedContext, j as MetaServerConfig, k as PosthogServerCredentials, m as EventContext, v as ServerTrackingOptions, y as createTracking, z as ga4 } from "./browser-34-CHcxK.js";
2
- export { EventContext, type Ga4ServerConfig, type Ga4ServerCredentials, type LinkedinServerConfig, type LinkedinServerCredentials, type MetaServerConfig, type MetaServerCredentials, type PosthogServerConfig, type PosthogServerCredentials, ResolvedContext, ServerProvider, ServerTracking, ServerTrackingOptions, createTracking, ga4, linkedin, meta, posthog };
1
+ import { A as PosthogServerCredentials, B as ga4, F as LinkedinServerConfig, I as LinkedinServerCredentials, L as linkedin, M as MetaServerConfig, N as MetaServerCredentials, P as meta, R as Ga4ServerConfig, _ as ServerProvider, b as createTracking, g as ResolvedContext, h as IdentifyContext, j as posthog, k as PosthogServerConfig, m as EventContext, v as ServerTracking, y as ServerTrackingOptions, z as Ga4ServerCredentials } from "./browser-k4gaaQgF.js";
2
+ export { EventContext, type Ga4ServerConfig, type Ga4ServerCredentials, IdentifyContext, type LinkedinServerConfig, type LinkedinServerCredentials, type MetaServerConfig, type MetaServerCredentials, type PosthogServerConfig, type PosthogServerCredentials, ResolvedContext, ServerProvider, ServerTracking, ServerTrackingOptions, createTracking, ga4, linkedin, meta, posthog };
package/dist/server.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as ga4, i as linkedin, n as posthog, r as meta, t as createTracking } from "./server-DnP7RtEn.js";
1
+ import { a as ga4, i as linkedin, n as posthog, r as meta, t as createTracking } from "./server-bgUN2MOi.js";
2
2
  export { createTracking, ga4, linkedin, meta, posthog };