@blotoutio/providers-universal-ads-sdk 1.66.0 → 1.66.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/index.cjs.js +405 -91
  2. package/index.js +405 -91
  3. package/index.mjs +405 -91
  4. package/package.json +1 -1
package/index.cjs.js CHANGED
@@ -1,6 +1,159 @@
1
1
  'use strict';
2
2
 
3
+ /**
4
+ * The Universal Ads pixel vocabulary.
5
+ *
6
+ * Every name here is taken verbatim from Universal Ads' own tooling — the
7
+ * event-level snippets the UA dashboard generates and the official GTM
8
+ * community template (`Universal-Ads/universal-ads-pixel-gtm`, `template.tpl`).
9
+ * uap.js lower-cases the conversion name before it posts it (`eventName:
10
+ * name.toLowerCase()`) but validates nothing else and aliases nothing, so a
11
+ * near-miss (`start_checkout` instead of `checkout_start`) is accepted by the
12
+ * pixel and then silently records nothing. Do not "tidy" these strings.
13
+ */
14
+ /** All 18 conversions the pixel accepts, including the two custom slots. */
15
+ const UA_CONVERSIONS = [
16
+ 'page_view',
17
+ 'content_view',
18
+ 'product_view',
19
+ 'search',
20
+ 'sign_in',
21
+ 'sign_up',
22
+ 'schedule_appointment',
23
+ 'start_trial',
24
+ 'view_cart',
25
+ 'wish_list',
26
+ 'purchase',
27
+ 'checkout_start',
28
+ 'add_to_cart',
29
+ 'add_payment_info',
30
+ 'add_address_info',
31
+ 'subscribe',
32
+ 'custom_event_1',
33
+ 'custom_event_2',
34
+ ];
35
+ /**
36
+ * Which parameters UA offers per event, mirroring the event-level snippets the
37
+ * dashboard generates. `page_view` is carried by the base code and takes none.
38
+ */
39
+ const UA_EVENT_PARAMS = {
40
+ page_view: [],
41
+ content_view: ['description', 'content_id', 'user_id'],
42
+ product_view: ['price', 'currency', 'description', 'content_id', 'user_id'],
43
+ search: ['price', 'currency', 'description', 'content_id', 'user_id'],
44
+ sign_in: ['description', 'user_id', 'user_name'],
45
+ sign_up: ['description', 'user_id', 'user_name'],
46
+ schedule_appointment: ['description', 'user_id', 'user_name'],
47
+ start_trial: ['price', 'currency', 'description', 'user_id', 'user_name'],
48
+ subscribe: ['price', 'currency', 'description', 'user_id', 'user_name'],
49
+ view_cart: [
50
+ 'price',
51
+ 'currency',
52
+ 'description',
53
+ 'content_id',
54
+ 'item_quantity',
55
+ 'user_id',
56
+ ],
57
+ wish_list: [
58
+ 'price',
59
+ 'currency',
60
+ 'description',
61
+ 'content_id',
62
+ 'item_quantity',
63
+ 'user_id',
64
+ ],
65
+ checkout_start: [
66
+ 'price',
67
+ 'currency',
68
+ 'description',
69
+ 'content_id',
70
+ 'item_quantity',
71
+ 'user_id',
72
+ ],
73
+ add_to_cart: [
74
+ 'price',
75
+ 'currency',
76
+ 'description',
77
+ 'content_id',
78
+ 'item_quantity',
79
+ 'user_id',
80
+ ],
81
+ add_payment_info: [
82
+ 'price',
83
+ 'currency',
84
+ 'description',
85
+ 'content_id',
86
+ 'item_quantity',
87
+ 'user_id',
88
+ ],
89
+ add_address_info: [
90
+ 'price',
91
+ 'currency',
92
+ 'description',
93
+ 'content_id',
94
+ 'item_quantity',
95
+ 'user_id',
96
+ ],
97
+ purchase: [
98
+ 'price',
99
+ 'currency',
100
+ 'order_id',
101
+ 'description',
102
+ 'content_id',
103
+ 'item_quantity',
104
+ 'user_id',
105
+ 'user_name',
106
+ ],
107
+ custom_event_1: [
108
+ 'price',
109
+ 'currency',
110
+ 'order_id',
111
+ 'description',
112
+ 'content_id',
113
+ 'item_quantity',
114
+ 'user_id',
115
+ 'user_name',
116
+ ],
117
+ custom_event_2: [
118
+ 'price',
119
+ 'currency',
120
+ 'order_id',
121
+ 'description',
122
+ 'content_id',
123
+ 'item_quantity',
124
+ 'user_id',
125
+ 'user_name',
126
+ ],
127
+ };
128
+ /**
129
+ * EdgeTag event -> UA conversion, applied when a tag configures no override.
130
+ *
131
+ * `RemoveFromCart` and `SessionStart` are absent on purpose: UA has no
132
+ * equivalent, so they are dropped rather than mapped onto something adjacent.
133
+ * The UA conversions with no EdgeTag twin (sign_in, product_view, start_trial,
134
+ * view_cart, wish_list, schedule_appointment, custom_event_1/2) are reachable
135
+ * through the per-tag event mapping.
136
+ */
137
+ const DEFAULT_EVENT_MAP = {
138
+ PageView: 'page_view',
139
+ ViewContent: 'content_view',
140
+ AddToCart: 'add_to_cart',
141
+ InitiateCheckout: 'checkout_start',
142
+ AddPaymentInfo: 'add_payment_info',
143
+ AddShippingInfo: 'add_address_info',
144
+ Purchase: 'purchase',
145
+ Subscribe: 'subscribe',
146
+ Search: 'search',
147
+ Lead: 'sign_up',
148
+ };
149
+
3
150
  const packageName = 'universalAds';
151
+ // The pixel library UA serves; it aliases itself onto `window[data-sdk-name]`.
152
+ const SDK_URL = 'https://connect.universalads.com/uap.js';
153
+ // The global the pixel is reachable through once loaded with our script tag.
154
+ const SDK_NAME = 'uapx';
155
+
156
+ const keyPrefix = `_worker`;
4
157
 
5
158
  const canLog = () => {
6
159
  try {
@@ -34,6 +187,176 @@ const logger = {
34
187
  },
35
188
  };
36
189
 
190
+ const initKey = `${keyPrefix}StoreMultiple`;
191
+ const getData = (destination, persistType, key = initKey) => {
192
+ let data;
193
+ if (persistType === 'session') {
194
+ data = getSession(key);
195
+ }
196
+ else {
197
+ data = getLocal(key);
198
+ }
199
+ return (data === null || data === void 0 ? void 0 : data[destination]) || {};
200
+ };
201
+ const getLocal = (key) => {
202
+ try {
203
+ if (!localStorage) {
204
+ return {};
205
+ }
206
+ const data = localStorage.getItem(key);
207
+ if (!data) {
208
+ return {};
209
+ }
210
+ return JSON.parse(data) || {};
211
+ }
212
+ catch {
213
+ return {};
214
+ }
215
+ };
216
+ const getSession = (key) => {
217
+ try {
218
+ if (!sessionStorage) {
219
+ return {};
220
+ }
221
+ const data = sessionStorage.getItem(key);
222
+ if (!data) {
223
+ return {};
224
+ }
225
+ return JSON.parse(data) || {};
226
+ }
227
+ catch {
228
+ return {};
229
+ }
230
+ };
231
+
232
+ const SCRIPT_ID = 'uapx-sdk';
233
+ /**
234
+ * Conversions raised before uap.js finished loading.
235
+ *
236
+ * The pixel script is async, so EdgeTag's PageView routinely reaches `tag`
237
+ * before `window.uapx` exists. UA's own snippet dodges this by sending
238
+ * page_view from inside `onload`; we send from the tag handler instead, so we
239
+ * buffer here rather than dropping the event.
240
+ */
241
+ const pending = [];
242
+ /**
243
+ * Cap on the queue. An ad blocker blocking `connect.universalads.com` is the
244
+ * expected failure for an ad pixel, and an SPA session can raise events for
245
+ * hours — the queue must not grow with them.
246
+ *
247
+ * @internal Exported for the cap assertion in `pixel.test.ts` only.
248
+ */
249
+ const MAX_PENDING = 50;
250
+ /**
251
+ * Set once the pixel can never arrive: the script errored, or it loaded without
252
+ * exposing the global. From then on conversions are dropped instead of queued
253
+ * for a flush that will never come.
254
+ */
255
+ let unavailable = false;
256
+ /** Give up for the rest of the page: release the queue and say why once. */
257
+ const abandon = (message) => {
258
+ unavailable = true;
259
+ pending.length = 0;
260
+ logger.log(message);
261
+ };
262
+ const flush = () => {
263
+ var _a;
264
+ while (pending.length) {
265
+ const next = pending.shift();
266
+ if (!next) {
267
+ return;
268
+ }
269
+ // One rejected conversion must not strand the rest of the queue.
270
+ try {
271
+ (_a = window.uapx) === null || _a === void 0 ? void 0 : _a.sendConversion(next.conversion, next.params);
272
+ }
273
+ catch (error) {
274
+ logger.error(error);
275
+ }
276
+ }
277
+ };
278
+ const start = (productId, accessToken) => {
279
+ if (!window.uapx) {
280
+ abandon('Universal Ads pixel loaded but window.uapx is missing; conversions will not be sent.');
281
+ return;
282
+ }
283
+ const config = {
284
+ general: {
285
+ install_id_enabled: true,
286
+ pers_tr: true,
287
+ collect_page_info: true,
288
+ collect_ga: true,
289
+ product_id: productId,
290
+ access_token: accessToken,
291
+ },
292
+ };
293
+ // This runs from the script's `onload`, outside init's try/catch, so a throw
294
+ // here would surface as an uncaught error on the customer's page.
295
+ try {
296
+ window.uapx.startWithConfig(config);
297
+ }
298
+ catch (error) {
299
+ logger.error(error);
300
+ }
301
+ flush();
302
+ };
303
+ const loadPixel = (productId, accessToken) => {
304
+ // Already present, either from a previous init or a hand-placed base snippet.
305
+ // Starting twice would re-run session bookkeeping inside uap.js.
306
+ if (window.uapx || document.getElementById(SCRIPT_ID)) {
307
+ return;
308
+ }
309
+ const script = document.createElement('script');
310
+ script.id = SCRIPT_ID;
311
+ script.type = 'text/javascript';
312
+ script.async = true;
313
+ // uap.js reads this off `document.currentScript` to decide which global to
314
+ // alias itself onto. Without it the pixel is only reachable as
315
+ // `window.pubSuite` and every sendConversion call below silently no-ops.
316
+ script.setAttribute('data-sdk-name', SDK_NAME);
317
+ script.src = SDK_URL;
318
+ script.onload = () => {
319
+ start(productId, accessToken);
320
+ };
321
+ script.onerror = () => {
322
+ script.remove();
323
+ abandon(`Universal Ads pixel failed to load from ${SDK_URL}.`);
324
+ };
325
+ document.head.appendChild(script);
326
+ };
327
+ const sendConversion = (conversion, params) => {
328
+ if (window.uapx) {
329
+ window.uapx.sendConversion(conversion, params);
330
+ return;
331
+ }
332
+ if (unavailable || pending.length >= MAX_PENDING) {
333
+ return;
334
+ }
335
+ pending.push({ conversion, params });
336
+ };
337
+
338
+ const init = ({ manifest }) => {
339
+ var _a;
340
+ try {
341
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
342
+ return;
343
+ }
344
+ if (((_a = manifest.variables) === null || _a === void 0 ? void 0 : _a['enableBrowser']) !== '1') {
345
+ return;
346
+ }
347
+ const productId = manifest.variables['productId'];
348
+ const accessToken = manifest.variables['accessToken'];
349
+ if (!productId || !accessToken) {
350
+ logger.log('Universal Ads pixel is not loaded. Configure UNIVERSAL_ADS_PRODUCT_ID and UNIVERSAL_ADS_ACCESS_TOKEN.');
351
+ return;
352
+ }
353
+ loadPixel(productId, accessToken);
354
+ }
355
+ catch (error) {
356
+ logger.error(error);
357
+ }
358
+ };
359
+
37
360
  /**
38
361
  * Known ad-network click ID query parameters and the human-readable provider
39
362
  * label each one maps to. Used by the CDN worker to resolve `inSessionTouch`
@@ -460,26 +783,6 @@ new Set([
460
783
  ...caProvinces.keys(),
461
784
  ]);
462
785
 
463
- const normalize = (urlString) => {
464
- try {
465
- const cleanUrl = urlString.replace(/\t/g, '');
466
- const urlObj = new URL(cleanUrl);
467
- const pathname = urlObj.pathname.replace(/\/+$/, ''); // Remove trailing slash
468
- return `${urlObj.origin}${pathname}`;
469
- }
470
- catch (e) {
471
- // Fallback for non-URL values
472
- return urlString.replace(/\t/g, '').replace(/\/+$/, '');
473
- }
474
- };
475
- // Convert wildcard URL pattern to RegExp (e.g. * becomes .+?)
476
- const wildcardToRegex = (pattern) => {
477
- const escaped = pattern
478
- .replace(/[-/\\^$+?.()|[\]{}]/g, '\\$&') // Escape regex special characters
479
- .replace(/\*/g, '[^/]+'); // Wildcard * matches one path segment
480
- return new RegExp(`^${escaped}$`, 'i'); // Case-insensitive match
481
- };
482
-
483
786
  /**
484
787
  * Exact utm_source normalization (lowercase key → canonical name).
485
788
  *
@@ -601,94 +904,105 @@ new Set([
601
904
  OFFLINE_TOUCH,
602
905
  ]);
603
906
 
604
- const getUrlMapping = (urlMapping, url) => {
605
- const mapping = tryParse(urlMapping, []);
606
- if (!mapping.length) {
607
- logger.log('Invalid urlMapping JSON:', urlMapping);
608
- return null;
609
- }
610
- const normalizedUrl = normalize(url);
611
- const match = mapping.find((item) => {
612
- // Skip incomplete rows rather than firing a pixel with `undefined` params.
613
- if (!item.url || !item.tagId || !item.segmentKey) {
614
- return false;
615
- }
616
- const normalizedPattern = normalize(item.url);
617
- if (normalizedPattern.includes('*')) {
618
- const regex = wildcardToRegex(normalizedPattern);
619
- return regex.test(normalizedUrl);
907
+ const isConversion = (value) => UA_CONVERSIONS.includes(value);
908
+ // A configured row wins for the event it names; every other event keeps its default.
909
+ const resolveConversion = (eventName, eventMapping) => {
910
+ var _a;
911
+ const configured = tryParse(eventMapping, []);
912
+ if (Array.isArray(configured)) {
913
+ const override = configured.find((row) => (row === null || row === void 0 ? void 0 : row.event) === eventName);
914
+ // A row naming a conversion UA does not accept is ignored rather than
915
+ // dropping the event: the form's conversion select is not creatable, so this
916
+ // only reaches us from a legacy or hand-edited worker value, and the default
917
+ // conversion is a better outcome there than silently sending nothing.
918
+ if (override && isConversion(override.conversion)) {
919
+ return override.conversion;
620
920
  }
621
- return normalizedUrl === normalizedPattern;
622
- });
623
- return match ? { tagId: match.tagId, segmentKey: match.segmentKey } : null;
921
+ }
922
+ return (_a = DEFAULT_EVENT_MAP[eventName]) !== null && _a !== void 0 ? _a : null;
624
923
  };
625
-
626
- const getUrl = (mapping, buzzKey, accountId) => {
627
- const cacheBuster = Math.floor(Math.random() * 10000000000);
628
- const url = new URL('https://cnv.event.prod.bidr.io/log/cnv');
629
- url.searchParams.set('tag_id', mapping.tagId);
630
- url.searchParams.set('buzz_key', buzzKey);
631
- url.searchParams.set('value', '');
632
- url.searchParams.set('segment_key', mapping.segmentKey);
633
- url.searchParams.set('account_id', accountId);
634
- url.searchParams.set('ord', cacheBuster.toString());
635
- return url.toString();
924
+ const contentsOf = (data) => Array.isArray(data['contents']) ? data['contents'] : [];
925
+ const asParam = (value) => {
926
+ if (value === undefined || value === null || value === '') {
927
+ return undefined;
928
+ }
929
+ if (typeof value === 'number') {
930
+ return Number.isFinite(value) ? String(value) : undefined;
931
+ }
932
+ return typeof value === 'string' ? value : undefined;
636
933
  };
637
- const initUniversalAds = (accountId, buzzKey, urlMapping, pageUrl) => {
638
- var _a;
639
- if (typeof document == 'undefined') {
640
- return;
934
+ const itemQuantity = (contents) => {
935
+ if (!contents.length) {
936
+ return undefined;
641
937
  }
642
- let url;
643
- if (pageUrl) {
644
- try {
645
- const parsed = new URL(pageUrl);
646
- url = parsed.origin + parsed.pathname;
647
- }
648
- catch {
649
- url = window.location.origin + window.location.pathname;
938
+ const total = contents.reduce((sum, item) => {
939
+ const quantity = Number(item === null || item === void 0 ? void 0 : item.quantity);
940
+ return sum + (Number.isFinite(quantity) ? quantity : 0);
941
+ }, 0);
942
+ return total || undefined;
943
+ };
944
+ // Only the parameters UA allows for that conversion; empty values are dropped.
945
+ const buildParams = (conversion, data, userId, kvEmail) => {
946
+ var _a, _b, _c;
947
+ const contents = contentsOf(data);
948
+ const available = {
949
+ price: asParam(data['value']),
950
+ currency: asParam(data['currency']),
951
+ order_id: asParam(data['orderId']),
952
+ description: (_a = asParam(data['name'])) !== null && _a !== void 0 ? _a : asParam((_b = contents[0]) === null || _b === void 0 ? void 0 : _b.title),
953
+ // UA's snippets carry a single content id, so a multi-item cart reports the
954
+ // first usable one rather than a list UA would not parse.
955
+ content_id: contents.map((item) => asParam(item === null || item === void 0 ? void 0 : item.id)).find(Boolean),
956
+ item_quantity: asParam(itemQuantity(contents)),
957
+ user_id: asParam(userId),
958
+ // UA labels `user_name` "User Email" in both the dashboard and the GTM
959
+ // template — it carries the email, not a display name. It goes out in the
960
+ // clear on purpose: UA's dashboard snippets and their GTM template both pass
961
+ // the plain address and uap.js normalizes it on their side, so a sha256'd
962
+ // value (as criteo/spotify need) would simply fail to match. Same shape as
963
+ // facebook's `em` and tiktok's `email` here.
964
+ //
965
+ // The event rarely carries it — email usually arrives through the data API
966
+ // and lands in session KV — so fall back to the KV value.
967
+ user_name: (_c = asParam(data['email'])) !== null && _c !== void 0 ? _c : asParam(kvEmail),
968
+ };
969
+ const params = {};
970
+ for (const key of UA_EVENT_PARAMS[conversion]) {
971
+ const value = available[key];
972
+ if (value !== undefined) {
973
+ params[key] = value;
650
974
  }
651
975
  }
652
- else {
653
- url = window.location.origin + window.location.pathname;
654
- }
976
+ return params;
977
+ };
978
+
979
+ const tag = ({ eventName, data, userId, manifestVariables, destination, }) => {
655
980
  try {
656
- const mapping = getUrlMapping(urlMapping, url);
657
- if (mapping) {
658
- const pixel = document.createElement('img');
659
- pixel.src = getUrl(mapping, buzzKey, accountId);
660
- pixel.width = 0;
661
- pixel.height = 0;
662
- pixel.style.position = 'absolute';
663
- pixel.style.visibility = 'hidden';
664
- pixel.setAttribute('border', '0');
665
- const firstScript = document.getElementsByTagName('script')[0];
666
- (_a = firstScript.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(pixel, firstScript);
981
+ if (typeof window === 'undefined') {
982
+ return {};
667
983
  }
668
- else {
669
- logger.log('URL is not mapped for Universal Ads');
984
+ const conversion = resolveConversion(eventName, manifestVariables === null || manifestVariables === void 0 ? void 0 : manifestVariables['eventMapping']);
985
+ // UA has no conversion for this event (RemoveFromCart, SessionStart, or an
986
+ // unmapped custom event) — sending an invented name would record nothing.
987
+ if (!conversion) {
988
+ return {};
670
989
  }
990
+ // The email UA wants for `user_name` usually arrives via the data API and is
991
+ // held in session KV rather than on the event payload.
992
+ const kv = getData(destination, 'session')['kv'];
993
+ sendConversion(conversion, buildParams(conversion, data, userId, kv === null || kv === void 0 ? void 0 : kv['email']));
671
994
  }
672
- catch {
673
- logger.log('Not able to parse the URL mapping for Universal Ads');
674
- }
675
- };
676
- const init = ({ manifest, pageUrl }) => {
677
- if (typeof window == 'undefined' ||
678
- !manifest.variables ||
679
- manifest.variables['enableBrowser'] !== '1' ||
680
- !manifest.variables['accountId'] ||
681
- !manifest.variables['buzzKey'] ||
682
- !manifest.variables['urlMapping']) {
683
- return;
995
+ catch (error) {
996
+ logger.error(error);
684
997
  }
685
- initUniversalAds(manifest.variables['accountId'], manifest.variables['buzzKey'], manifest.variables['urlMapping'], pageUrl);
998
+ return {};
686
999
  };
687
1000
 
688
1001
  // eslint-disable-next-line @nx/enforce-module-boundaries
689
1002
  const data = {
690
1003
  name: packageName,
691
1004
  init,
1005
+ tag,
692
1006
  };
693
1007
  try {
694
1008
  if (window) {