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