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