@blotoutio/providers-app-lovin-sdk 1.71.1 → 1.72.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.cjs.js +224 -35
- package/index.js +224 -35
- package/index.mjs +224 -35
- package/package.json +1 -1
package/index.cjs.js
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* AppLovin's `user_data.zip` is documented as "The user's billing zip code. Use
|
|
5
|
+
* only the first 5 digits for US zip codes", so a ZIP+4 arriving from a billing
|
|
6
|
+
* address is truncated to its first five digits.
|
|
7
|
+
*
|
|
8
|
+
* Only for US values. A blind five-character slice would corrupt every other
|
|
9
|
+
* postal format — UK `SW1A 1AA` becomes `SW1A `, Canadian `K1A 0B1` becomes
|
|
10
|
+
* `K1A 0` — so anything else passes through as given.
|
|
11
|
+
*
|
|
12
|
+
* Shared by the cdn and sdk paths so both send the same value.
|
|
13
|
+
*/
|
|
14
|
+
const getNormalizedZip = (zip, country) => {
|
|
15
|
+
var _a;
|
|
16
|
+
const trimmed = zip === null || zip === void 0 ? void 0 : zip.trim();
|
|
17
|
+
if (!trimmed) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
// ISO 3166 per the same spec; `USA` is accepted because billing addresses
|
|
21
|
+
// supply both spellings.
|
|
22
|
+
const isUS = ['US', 'USA'].includes((_a = country === null || country === void 0 ? void 0 : country.trim().toUpperCase()) !== null && _a !== void 0 ? _a : '');
|
|
23
|
+
// The country is optional on both call paths, and the ZIP+4 shape belongs to
|
|
24
|
+
// no other postal format, so a value in that shape is truncated even when no
|
|
25
|
+
// country accompanies it.
|
|
26
|
+
const isZipPlusFour = /^\d{5}-?\d{4}$/.test(trimmed);
|
|
27
|
+
if (!isUS && !isZipPlusFour) {
|
|
28
|
+
return trimmed;
|
|
29
|
+
}
|
|
30
|
+
return trimmed.replace(/\D/g, '').slice(0, 5) || undefined;
|
|
31
|
+
};
|
|
32
|
+
|
|
3
33
|
const packageName = 'appLovin';
|
|
4
34
|
|
|
5
35
|
const t = [
|
|
@@ -121,6 +151,20 @@ const getSession = (key) => {
|
|
|
121
151
|
}
|
|
122
152
|
};
|
|
123
153
|
|
|
154
|
+
const getIsNewCustomerFlag = async (getData) => {
|
|
155
|
+
const { isNewCustomer } = await new Promise((resolve) => getData(['isNewCustomer'], resolve));
|
|
156
|
+
return isNewCustomer ? isNewCustomer === 'true' : undefined;
|
|
157
|
+
};
|
|
158
|
+
// Reads the isNewCustomer flag already persisted in the browser's session KV
|
|
159
|
+
// (written by the `data` API call, which runs before the Purchase event). Lets
|
|
160
|
+
// callers use the local value and avoid a redundant edge round-trip; returns
|
|
161
|
+
// undefined when the value has not been stored yet so the caller can fall back.
|
|
162
|
+
const getIsNewCustomerFromBrowser = (destination) => {
|
|
163
|
+
const kv = getData(destination, 'session')['kv'];
|
|
164
|
+
const isNewCustomer = kv === null || kv === void 0 ? void 0 : kv['isNewCustomer'];
|
|
165
|
+
return isNewCustomer ? isNewCustomer === 'true' : undefined;
|
|
166
|
+
};
|
|
167
|
+
|
|
124
168
|
/**
|
|
125
169
|
* Known ad-network click ID query parameters and the human-readable provider
|
|
126
170
|
* label each one maps to. Used by the CDN worker to resolve `inSessionTouch`
|
|
@@ -158,6 +202,18 @@ const CLICK_IDS = [
|
|
|
158
202
|
{ param: 'duel_a', label: 'Duel' }, // Duel referral/advocacy
|
|
159
203
|
];
|
|
160
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Checks whether a value is a plain JavaScript object (created via `{}` or `new Object()`).
|
|
207
|
+
* Returns false for arrays, class instances, null, and objects with modified prototypes.
|
|
208
|
+
* Note: Objects created with `Object.create(null)` will return false.
|
|
209
|
+
*/
|
|
210
|
+
const isPlainObject = (value) => {
|
|
211
|
+
if (typeof value !== 'object' || value === null) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
return Object.getPrototypeOf(value) === Object.prototype;
|
|
215
|
+
};
|
|
216
|
+
|
|
161
217
|
const expand = (str) => str.split(',').flatMap((entry) => {
|
|
162
218
|
if (!entry.includes('-')) {
|
|
163
219
|
return entry;
|
|
@@ -223,6 +279,18 @@ const normalizeEmail = (email) => {
|
|
|
223
279
|
return trimmedEmail;
|
|
224
280
|
};
|
|
225
281
|
|
|
282
|
+
function tryParse(json, fallbackValue) {
|
|
283
|
+
try {
|
|
284
|
+
if (json) {
|
|
285
|
+
return JSON.parse(json);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
/* do nothing */
|
|
290
|
+
}
|
|
291
|
+
return fallbackValue;
|
|
292
|
+
}
|
|
293
|
+
|
|
226
294
|
/**
|
|
227
295
|
* ISO-3166 2-leter country codes and their names
|
|
228
296
|
* @see https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
|
|
@@ -569,6 +637,78 @@ new Set([
|
|
|
569
637
|
...caProvinces.keys(),
|
|
570
638
|
]);
|
|
571
639
|
|
|
640
|
+
/**
|
|
641
|
+
* Self-describing JSON compaction codec (`packMappings` / `unpackMappings`).
|
|
642
|
+
*
|
|
643
|
+
* Worker variables have a 5KB-per-variable limit, and objects waste most of it
|
|
644
|
+
* repeating long field names on every row. `packMappings` rewrites every object
|
|
645
|
+
* key (at any depth) to a short auto-derived token and carries a single embedded
|
|
646
|
+
* legend, so the long name is stored once. `unpackMappings` reverses it using
|
|
647
|
+
* that legend alone — there is NO external key-map to maintain per provider.
|
|
648
|
+
*
|
|
649
|
+
* Works on ANY JSON value: a mapping array (the common case), a single plain
|
|
650
|
+
* object, nested structures, or primitives all round-trip.
|
|
651
|
+
*
|
|
652
|
+
* The wire format is a versioned envelope:
|
|
653
|
+
* { "~": 1, "k": { <short>: <long>, ... }, "d": <transformed value> }
|
|
654
|
+
*
|
|
655
|
+
* Guarantees (all covered by tests):
|
|
656
|
+
* - `unpackMappings(packMappings(x))` deep-equals `x` with empty fields removed,
|
|
657
|
+
* for any JSON value (objects, arrays, nesting, primitives).
|
|
658
|
+
* - Short keys are always unique — long names sharing a prefix are disambiguated
|
|
659
|
+
* automatically, so `packMappings` never loses or merges a field.
|
|
660
|
+
* - `unpackMappings` returns legacy plain JSON (no envelope) unchanged, so values
|
|
661
|
+
* written before compaction keep decoding; invalid/empty input → `undefined`.
|
|
662
|
+
*
|
|
663
|
+
* The building blocks (`isEmptyValue`, `deriveShortKey`, `encodeMappings`,
|
|
664
|
+
* `decodeMappings`, `isMappingEnvelope`) are exported as pure functions so each
|
|
665
|
+
* is independently testable.
|
|
666
|
+
*/
|
|
667
|
+
const CODEC_VERSION = 1;
|
|
668
|
+
const VERSION_KEY = '~';
|
|
669
|
+
const LEGEND_KEY = 'k';
|
|
670
|
+
const DATA_KEY = 'd';
|
|
671
|
+
/**
|
|
672
|
+
* Pure inverse of `encodeMappings`: rewrites every object key back to its long
|
|
673
|
+
* name via the legend (unknown keys pass through). Arrays and primitives pass
|
|
674
|
+
* through structurally.
|
|
675
|
+
*/
|
|
676
|
+
const decodeMappings = (data, legend) => {
|
|
677
|
+
var _a;
|
|
678
|
+
if (Array.isArray(data)) {
|
|
679
|
+
return data.map((item) => decodeMappings(item, legend));
|
|
680
|
+
}
|
|
681
|
+
if (isPlainObject(data)) {
|
|
682
|
+
const result = {};
|
|
683
|
+
for (const [key, val] of Object.entries(data)) {
|
|
684
|
+
result[(_a = legend[key]) !== null && _a !== void 0 ? _a : key] = decodeMappings(val, legend);
|
|
685
|
+
}
|
|
686
|
+
return result;
|
|
687
|
+
}
|
|
688
|
+
return data;
|
|
689
|
+
};
|
|
690
|
+
/** True when a parsed value is a `packMappings` envelope (vs. legacy plain JSON). */
|
|
691
|
+
const isMappingEnvelope = (value) => isPlainObject(value) &&
|
|
692
|
+
value[VERSION_KEY] === CODEC_VERSION &&
|
|
693
|
+
isPlainObject(value[LEGEND_KEY]) &&
|
|
694
|
+
DATA_KEY in value;
|
|
695
|
+
function unpackMappings(text, fallback) {
|
|
696
|
+
const parsed = tryParse(text);
|
|
697
|
+
if (parsed === undefined || parsed === null) {
|
|
698
|
+
return fallback;
|
|
699
|
+
}
|
|
700
|
+
if (isMappingEnvelope(parsed)) {
|
|
701
|
+
return decodeMappings(parsed[DATA_KEY], parsed[LEGEND_KEY]);
|
|
702
|
+
}
|
|
703
|
+
// A versioned envelope this reader can't decode (e.g. a future CODEC_VERSION
|
|
704
|
+
// written by a newer UI while this worker still lags). Returning the raw
|
|
705
|
+
// `{~,k,d}` object would be silent corruption, so fall back instead.
|
|
706
|
+
if (isPlainObject(parsed) && VERSION_KEY in parsed) {
|
|
707
|
+
return fallback;
|
|
708
|
+
}
|
|
709
|
+
return parsed;
|
|
710
|
+
}
|
|
711
|
+
|
|
572
712
|
/**
|
|
573
713
|
* Exact utm_source normalization (lowercase key → canonical name).
|
|
574
714
|
*
|
|
@@ -691,24 +831,17 @@ new Set([
|
|
|
691
831
|
]);
|
|
692
832
|
|
|
693
833
|
// eslint-disable-next-line @nx/enforce-module-boundaries
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
logger.error(`Error parsing category: ${category}`, error);
|
|
701
|
-
return undefined;
|
|
702
|
-
}
|
|
703
|
-
};
|
|
834
|
+
// The same helpers as the cdn copy in cdn/src/lib/utils.ts, so a malformed
|
|
835
|
+
// mapping fails identically on both sides. `unpackMappings` yields the fallback
|
|
836
|
+
// for input that parses to `null`, and the array guard covers valid JSON that
|
|
837
|
+
// is not an array; either would otherwise reach `.map` and throw, which in
|
|
838
|
+
// browser SDK code takes the whole tag down.
|
|
839
|
+
const getParsedCategoryId = (category) => { var _a; return (_a = tryParse(category)) === null || _a === void 0 ? void 0 : _a.value; };
|
|
704
840
|
const getCategories = (categoryMapping) => {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
categoryMappings = JSON.parse(categoryMapping || '[]');
|
|
708
|
-
}
|
|
709
|
-
catch {
|
|
841
|
+
const categoryMappings = unpackMappings(categoryMapping, []);
|
|
842
|
+
if (!Array.isArray(categoryMappings)) {
|
|
710
843
|
logger.error('Unable to parse category mappings');
|
|
711
|
-
|
|
844
|
+
return new Map();
|
|
712
845
|
}
|
|
713
846
|
return new Map(categoryMappings.map(({ storeCategory, appLovinCategory }) => [
|
|
714
847
|
storeCategory.toLowerCase(),
|
|
@@ -745,6 +878,13 @@ const getEventUserData = (userId, data) => {
|
|
|
745
878
|
userData.phone = normalizedPhone;
|
|
746
879
|
}
|
|
747
880
|
}
|
|
881
|
+
const zip = getNormalizedZip(data.zip, data.country_code);
|
|
882
|
+
if (zip) {
|
|
883
|
+
userData.zip = zip;
|
|
884
|
+
}
|
|
885
|
+
if (data.country_code) {
|
|
886
|
+
userData.country_code = data.country_code;
|
|
887
|
+
}
|
|
748
888
|
return userData;
|
|
749
889
|
};
|
|
750
890
|
/**
|
|
@@ -756,9 +896,10 @@ const getEventUserData = (userId, data) => {
|
|
|
756
896
|
* @param defaultCategory - JSON string containing default category configuration with label and value
|
|
757
897
|
* @param categoryMapping - JSON string containing array of category mappings
|
|
758
898
|
* @param userId - The explicit user ID to use in event user_data
|
|
899
|
+
* @param isNewCustomer - New/returning buyer flag, applied to Purchase only
|
|
759
900
|
* @returns EventData payload if the event is supported, undefined otherwise
|
|
760
901
|
*/
|
|
761
|
-
const getEventData = ({ eventName, userData, data, defaultCategory, categoryMapping, userId, }) => {
|
|
902
|
+
const getEventData = ({ eventName, userData, data, defaultCategory, categoryMapping, userId, isNewCustomer, }) => {
|
|
762
903
|
var _a, _b, _c;
|
|
763
904
|
const categories = getCategories(categoryMapping);
|
|
764
905
|
const defaultCategoryId = getParsedCategoryId(defaultCategory);
|
|
@@ -857,6 +998,9 @@ const getEventData = ({ eventName, userData, data, defaultCategory, categoryMapp
|
|
|
857
998
|
shipping: (_b = data.shippingCost) !== null && _b !== void 0 ? _b : 0,
|
|
858
999
|
tax: (_c = data.taxCost) !== null && _c !== void 0 ? _c : 0,
|
|
859
1000
|
user_data: getEventUserData(userId, userData),
|
|
1001
|
+
...(typeof isNewCustomer === 'boolean' && {
|
|
1002
|
+
measurement: { is_new_customer: isNewCustomer },
|
|
1003
|
+
}),
|
|
860
1004
|
},
|
|
861
1005
|
};
|
|
862
1006
|
}
|
|
@@ -903,9 +1047,14 @@ const getEventData = ({ eventName, userData, data, defaultCategory, categoryMapp
|
|
|
903
1047
|
}
|
|
904
1048
|
};
|
|
905
1049
|
|
|
906
|
-
const
|
|
1050
|
+
const getString = (kv, key) => {
|
|
1051
|
+
const value = kv === null || kv === void 0 ? void 0 : kv[key];
|
|
1052
|
+
return typeof value === 'string' && value ? value : undefined;
|
|
1053
|
+
};
|
|
1054
|
+
const tag = ({ data, eventName, eventId, manifestVariables, userId, destination, getEdgeData, }) => {
|
|
1055
|
+
var _a, _b;
|
|
907
1056
|
const payload = {
|
|
908
|
-
sdkVersion: "1.
|
|
1057
|
+
sdkVersion: "1.72.0" ,
|
|
909
1058
|
};
|
|
910
1059
|
if (window.axon && manifestVariables['enableBrowser'] === '1') {
|
|
911
1060
|
try {
|
|
@@ -914,28 +1063,68 @@ const tag = ({ data, eventName, eventId, manifestVariables, userId, destination,
|
|
|
914
1063
|
// email/phone usually arrive via the data API (stored in session KV),
|
|
915
1064
|
// not on the event payload itself.
|
|
916
1065
|
const kv = getData(destination, 'session')['kv'];
|
|
1066
|
+
// Same identity set the CAPI attaches server-side, so the browser pixel
|
|
1067
|
+
// is not the weaker of the two paths on a shopper we already know.
|
|
917
1068
|
const userData = { user_id: userId };
|
|
918
|
-
|
|
919
|
-
|
|
1069
|
+
// Checked per source rather than after the `||`, so a non-string on the
|
|
1070
|
+
// payload cannot displace a valid string in KV.
|
|
1071
|
+
const email = (_a = getString(data, 'email')) !== null && _a !== void 0 ? _a : getString(kv, 'email');
|
|
1072
|
+
if (email) {
|
|
920
1073
|
userData.email = email;
|
|
921
1074
|
}
|
|
922
|
-
const phone = (
|
|
923
|
-
if (phone
|
|
1075
|
+
const phone = (_b = getString(data, 'phone')) !== null && _b !== void 0 ? _b : getString(kv, 'phone');
|
|
1076
|
+
if (phone) {
|
|
924
1077
|
userData.phone = phone;
|
|
925
1078
|
}
|
|
926
|
-
const
|
|
927
|
-
|
|
928
|
-
userData
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
1079
|
+
const zip = getString(kv, 'zip');
|
|
1080
|
+
if (zip) {
|
|
1081
|
+
userData.zip = zip;
|
|
1082
|
+
}
|
|
1083
|
+
const country = getString(kv, 'country');
|
|
1084
|
+
if (country) {
|
|
1085
|
+
userData.country_code = country;
|
|
1086
|
+
}
|
|
1087
|
+
const send = (isNewCustomer) => {
|
|
1088
|
+
var _a;
|
|
1089
|
+
const event = getEventData({
|
|
1090
|
+
eventName,
|
|
1091
|
+
userData,
|
|
1092
|
+
data,
|
|
1093
|
+
defaultCategory,
|
|
1094
|
+
categoryMapping,
|
|
1095
|
+
userId,
|
|
1096
|
+
isNewCustomer,
|
|
1097
|
+
});
|
|
1098
|
+
if (!event) {
|
|
1099
|
+
logger.log(`[${packageName}] ${eventName} not supported`);
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
(_a = window.axon) === null || _a === void 0 ? void 0 : _a.call(window, 'track', event.name, event.data, { dedupe_id: eventId });
|
|
1103
|
+
};
|
|
1104
|
+
if (eventName !== 'Purchase') {
|
|
1105
|
+
send();
|
|
1106
|
+
return payload;
|
|
1107
|
+
}
|
|
1108
|
+
// Prefer the flag already saved in the browser (the Shopify pixel's
|
|
1109
|
+
// `data` call writes it just before Purchase). Only query the edge when
|
|
1110
|
+
// the browser value is missing.
|
|
1111
|
+
const browserValue = destination
|
|
1112
|
+
? getIsNewCustomerFromBrowser(destination)
|
|
1113
|
+
: undefined;
|
|
1114
|
+
if (typeof browserValue === 'boolean') {
|
|
1115
|
+
send(browserValue);
|
|
1116
|
+
}
|
|
1117
|
+
else if (getEdgeData) {
|
|
1118
|
+
getIsNewCustomerFlag(getEdgeData)
|
|
1119
|
+
// The fallback is the rejection handler rather than a trailing
|
|
1120
|
+
// `catch`, so only a failed lookup sends without the flag; a `send`
|
|
1121
|
+
// that throws must not be retried into a duplicate pixel call.
|
|
1122
|
+
.then(send, () => send())
|
|
1123
|
+
.catch((error) => logger.error(error));
|
|
1124
|
+
}
|
|
1125
|
+
else {
|
|
1126
|
+
send();
|
|
937
1127
|
}
|
|
938
|
-
window.axon('track', event.name, event.data, { dedupe_id: eventId });
|
|
939
1128
|
}
|
|
940
1129
|
catch (e) {
|
|
941
1130
|
logger.error(e);
|
package/index.js
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
var ProvidersAppLovinSdk = (function () {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* AppLovin's `user_data.zip` is documented as "The user's billing zip code. Use
|
|
6
|
+
* only the first 5 digits for US zip codes", so a ZIP+4 arriving from a billing
|
|
7
|
+
* address is truncated to its first five digits.
|
|
8
|
+
*
|
|
9
|
+
* Only for US values. A blind five-character slice would corrupt every other
|
|
10
|
+
* postal format — UK `SW1A 1AA` becomes `SW1A `, Canadian `K1A 0B1` becomes
|
|
11
|
+
* `K1A 0` — so anything else passes through as given.
|
|
12
|
+
*
|
|
13
|
+
* Shared by the cdn and sdk paths so both send the same value.
|
|
14
|
+
*/
|
|
15
|
+
const getNormalizedZip = (zip, country) => {
|
|
16
|
+
var _a;
|
|
17
|
+
const trimmed = zip === null || zip === void 0 ? void 0 : zip.trim();
|
|
18
|
+
if (!trimmed) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
// ISO 3166 per the same spec; `USA` is accepted because billing addresses
|
|
22
|
+
// supply both spellings.
|
|
23
|
+
const isUS = ['US', 'USA'].includes((_a = country === null || country === void 0 ? void 0 : country.trim().toUpperCase()) !== null && _a !== void 0 ? _a : '');
|
|
24
|
+
// The country is optional on both call paths, and the ZIP+4 shape belongs to
|
|
25
|
+
// no other postal format, so a value in that shape is truncated even when no
|
|
26
|
+
// country accompanies it.
|
|
27
|
+
const isZipPlusFour = /^\d{5}-?\d{4}$/.test(trimmed);
|
|
28
|
+
if (!isUS && !isZipPlusFour) {
|
|
29
|
+
return trimmed;
|
|
30
|
+
}
|
|
31
|
+
return trimmed.replace(/\D/g, '').slice(0, 5) || undefined;
|
|
32
|
+
};
|
|
33
|
+
|
|
4
34
|
const packageName = 'appLovin';
|
|
5
35
|
|
|
6
36
|
const t = [
|
|
@@ -122,6 +152,20 @@ var ProvidersAppLovinSdk = (function () {
|
|
|
122
152
|
}
|
|
123
153
|
};
|
|
124
154
|
|
|
155
|
+
const getIsNewCustomerFlag = async (getData) => {
|
|
156
|
+
const { isNewCustomer } = await new Promise((resolve) => getData(['isNewCustomer'], resolve));
|
|
157
|
+
return isNewCustomer ? isNewCustomer === 'true' : undefined;
|
|
158
|
+
};
|
|
159
|
+
// Reads the isNewCustomer flag already persisted in the browser's session KV
|
|
160
|
+
// (written by the `data` API call, which runs before the Purchase event). Lets
|
|
161
|
+
// callers use the local value and avoid a redundant edge round-trip; returns
|
|
162
|
+
// undefined when the value has not been stored yet so the caller can fall back.
|
|
163
|
+
const getIsNewCustomerFromBrowser = (destination) => {
|
|
164
|
+
const kv = getData(destination, 'session')['kv'];
|
|
165
|
+
const isNewCustomer = kv === null || kv === void 0 ? void 0 : kv['isNewCustomer'];
|
|
166
|
+
return isNewCustomer ? isNewCustomer === 'true' : undefined;
|
|
167
|
+
};
|
|
168
|
+
|
|
125
169
|
/**
|
|
126
170
|
* Known ad-network click ID query parameters and the human-readable provider
|
|
127
171
|
* label each one maps to. Used by the CDN worker to resolve `inSessionTouch`
|
|
@@ -159,6 +203,18 @@ var ProvidersAppLovinSdk = (function () {
|
|
|
159
203
|
{ param: 'duel_a', label: 'Duel' }, // Duel referral/advocacy
|
|
160
204
|
];
|
|
161
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Checks whether a value is a plain JavaScript object (created via `{}` or `new Object()`).
|
|
208
|
+
* Returns false for arrays, class instances, null, and objects with modified prototypes.
|
|
209
|
+
* Note: Objects created with `Object.create(null)` will return false.
|
|
210
|
+
*/
|
|
211
|
+
const isPlainObject = (value) => {
|
|
212
|
+
if (typeof value !== 'object' || value === null) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
return Object.getPrototypeOf(value) === Object.prototype;
|
|
216
|
+
};
|
|
217
|
+
|
|
162
218
|
const expand = (str) => str.split(',').flatMap((entry) => {
|
|
163
219
|
if (!entry.includes('-')) {
|
|
164
220
|
return entry;
|
|
@@ -224,6 +280,18 @@ var ProvidersAppLovinSdk = (function () {
|
|
|
224
280
|
return trimmedEmail;
|
|
225
281
|
};
|
|
226
282
|
|
|
283
|
+
function tryParse(json, fallbackValue) {
|
|
284
|
+
try {
|
|
285
|
+
if (json) {
|
|
286
|
+
return JSON.parse(json);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
/* do nothing */
|
|
291
|
+
}
|
|
292
|
+
return fallbackValue;
|
|
293
|
+
}
|
|
294
|
+
|
|
227
295
|
/**
|
|
228
296
|
* ISO-3166 2-leter country codes and their names
|
|
229
297
|
* @see https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
|
|
@@ -570,6 +638,78 @@ var ProvidersAppLovinSdk = (function () {
|
|
|
570
638
|
...caProvinces.keys(),
|
|
571
639
|
]);
|
|
572
640
|
|
|
641
|
+
/**
|
|
642
|
+
* Self-describing JSON compaction codec (`packMappings` / `unpackMappings`).
|
|
643
|
+
*
|
|
644
|
+
* Worker variables have a 5KB-per-variable limit, and objects waste most of it
|
|
645
|
+
* repeating long field names on every row. `packMappings` rewrites every object
|
|
646
|
+
* key (at any depth) to a short auto-derived token and carries a single embedded
|
|
647
|
+
* legend, so the long name is stored once. `unpackMappings` reverses it using
|
|
648
|
+
* that legend alone — there is NO external key-map to maintain per provider.
|
|
649
|
+
*
|
|
650
|
+
* Works on ANY JSON value: a mapping array (the common case), a single plain
|
|
651
|
+
* object, nested structures, or primitives all round-trip.
|
|
652
|
+
*
|
|
653
|
+
* The wire format is a versioned envelope:
|
|
654
|
+
* { "~": 1, "k": { <short>: <long>, ... }, "d": <transformed value> }
|
|
655
|
+
*
|
|
656
|
+
* Guarantees (all covered by tests):
|
|
657
|
+
* - `unpackMappings(packMappings(x))` deep-equals `x` with empty fields removed,
|
|
658
|
+
* for any JSON value (objects, arrays, nesting, primitives).
|
|
659
|
+
* - Short keys are always unique — long names sharing a prefix are disambiguated
|
|
660
|
+
* automatically, so `packMappings` never loses or merges a field.
|
|
661
|
+
* - `unpackMappings` returns legacy plain JSON (no envelope) unchanged, so values
|
|
662
|
+
* written before compaction keep decoding; invalid/empty input → `undefined`.
|
|
663
|
+
*
|
|
664
|
+
* The building blocks (`isEmptyValue`, `deriveShortKey`, `encodeMappings`,
|
|
665
|
+
* `decodeMappings`, `isMappingEnvelope`) are exported as pure functions so each
|
|
666
|
+
* is independently testable.
|
|
667
|
+
*/
|
|
668
|
+
const CODEC_VERSION = 1;
|
|
669
|
+
const VERSION_KEY = '~';
|
|
670
|
+
const LEGEND_KEY = 'k';
|
|
671
|
+
const DATA_KEY = 'd';
|
|
672
|
+
/**
|
|
673
|
+
* Pure inverse of `encodeMappings`: rewrites every object key back to its long
|
|
674
|
+
* name via the legend (unknown keys pass through). Arrays and primitives pass
|
|
675
|
+
* through structurally.
|
|
676
|
+
*/
|
|
677
|
+
const decodeMappings = (data, legend) => {
|
|
678
|
+
var _a;
|
|
679
|
+
if (Array.isArray(data)) {
|
|
680
|
+
return data.map((item) => decodeMappings(item, legend));
|
|
681
|
+
}
|
|
682
|
+
if (isPlainObject(data)) {
|
|
683
|
+
const result = {};
|
|
684
|
+
for (const [key, val] of Object.entries(data)) {
|
|
685
|
+
result[(_a = legend[key]) !== null && _a !== void 0 ? _a : key] = decodeMappings(val, legend);
|
|
686
|
+
}
|
|
687
|
+
return result;
|
|
688
|
+
}
|
|
689
|
+
return data;
|
|
690
|
+
};
|
|
691
|
+
/** True when a parsed value is a `packMappings` envelope (vs. legacy plain JSON). */
|
|
692
|
+
const isMappingEnvelope = (value) => isPlainObject(value) &&
|
|
693
|
+
value[VERSION_KEY] === CODEC_VERSION &&
|
|
694
|
+
isPlainObject(value[LEGEND_KEY]) &&
|
|
695
|
+
DATA_KEY in value;
|
|
696
|
+
function unpackMappings(text, fallback) {
|
|
697
|
+
const parsed = tryParse(text);
|
|
698
|
+
if (parsed === undefined || parsed === null) {
|
|
699
|
+
return fallback;
|
|
700
|
+
}
|
|
701
|
+
if (isMappingEnvelope(parsed)) {
|
|
702
|
+
return decodeMappings(parsed[DATA_KEY], parsed[LEGEND_KEY]);
|
|
703
|
+
}
|
|
704
|
+
// A versioned envelope this reader can't decode (e.g. a future CODEC_VERSION
|
|
705
|
+
// written by a newer UI while this worker still lags). Returning the raw
|
|
706
|
+
// `{~,k,d}` object would be silent corruption, so fall back instead.
|
|
707
|
+
if (isPlainObject(parsed) && VERSION_KEY in parsed) {
|
|
708
|
+
return fallback;
|
|
709
|
+
}
|
|
710
|
+
return parsed;
|
|
711
|
+
}
|
|
712
|
+
|
|
573
713
|
/**
|
|
574
714
|
* Exact utm_source normalization (lowercase key → canonical name).
|
|
575
715
|
*
|
|
@@ -692,24 +832,17 @@ var ProvidersAppLovinSdk = (function () {
|
|
|
692
832
|
]);
|
|
693
833
|
|
|
694
834
|
// eslint-disable-next-line @nx/enforce-module-boundaries
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
logger.error(`Error parsing category: ${category}`, error);
|
|
702
|
-
return undefined;
|
|
703
|
-
}
|
|
704
|
-
};
|
|
835
|
+
// The same helpers as the cdn copy in cdn/src/lib/utils.ts, so a malformed
|
|
836
|
+
// mapping fails identically on both sides. `unpackMappings` yields the fallback
|
|
837
|
+
// for input that parses to `null`, and the array guard covers valid JSON that
|
|
838
|
+
// is not an array; either would otherwise reach `.map` and throw, which in
|
|
839
|
+
// browser SDK code takes the whole tag down.
|
|
840
|
+
const getParsedCategoryId = (category) => { var _a; return (_a = tryParse(category)) === null || _a === void 0 ? void 0 : _a.value; };
|
|
705
841
|
const getCategories = (categoryMapping) => {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
categoryMappings = JSON.parse(categoryMapping || '[]');
|
|
709
|
-
}
|
|
710
|
-
catch {
|
|
842
|
+
const categoryMappings = unpackMappings(categoryMapping, []);
|
|
843
|
+
if (!Array.isArray(categoryMappings)) {
|
|
711
844
|
logger.error('Unable to parse category mappings');
|
|
712
|
-
|
|
845
|
+
return new Map();
|
|
713
846
|
}
|
|
714
847
|
return new Map(categoryMappings.map(({ storeCategory, appLovinCategory }) => [
|
|
715
848
|
storeCategory.toLowerCase(),
|
|
@@ -746,6 +879,13 @@ var ProvidersAppLovinSdk = (function () {
|
|
|
746
879
|
userData.phone = normalizedPhone;
|
|
747
880
|
}
|
|
748
881
|
}
|
|
882
|
+
const zip = getNormalizedZip(data.zip, data.country_code);
|
|
883
|
+
if (zip) {
|
|
884
|
+
userData.zip = zip;
|
|
885
|
+
}
|
|
886
|
+
if (data.country_code) {
|
|
887
|
+
userData.country_code = data.country_code;
|
|
888
|
+
}
|
|
749
889
|
return userData;
|
|
750
890
|
};
|
|
751
891
|
/**
|
|
@@ -757,9 +897,10 @@ var ProvidersAppLovinSdk = (function () {
|
|
|
757
897
|
* @param defaultCategory - JSON string containing default category configuration with label and value
|
|
758
898
|
* @param categoryMapping - JSON string containing array of category mappings
|
|
759
899
|
* @param userId - The explicit user ID to use in event user_data
|
|
900
|
+
* @param isNewCustomer - New/returning buyer flag, applied to Purchase only
|
|
760
901
|
* @returns EventData payload if the event is supported, undefined otherwise
|
|
761
902
|
*/
|
|
762
|
-
const getEventData = ({ eventName, userData, data, defaultCategory, categoryMapping, userId, }) => {
|
|
903
|
+
const getEventData = ({ eventName, userData, data, defaultCategory, categoryMapping, userId, isNewCustomer, }) => {
|
|
763
904
|
var _a, _b, _c;
|
|
764
905
|
const categories = getCategories(categoryMapping);
|
|
765
906
|
const defaultCategoryId = getParsedCategoryId(defaultCategory);
|
|
@@ -858,6 +999,9 @@ var ProvidersAppLovinSdk = (function () {
|
|
|
858
999
|
shipping: (_b = data.shippingCost) !== null && _b !== void 0 ? _b : 0,
|
|
859
1000
|
tax: (_c = data.taxCost) !== null && _c !== void 0 ? _c : 0,
|
|
860
1001
|
user_data: getEventUserData(userId, userData),
|
|
1002
|
+
...(typeof isNewCustomer === 'boolean' && {
|
|
1003
|
+
measurement: { is_new_customer: isNewCustomer },
|
|
1004
|
+
}),
|
|
861
1005
|
},
|
|
862
1006
|
};
|
|
863
1007
|
}
|
|
@@ -904,9 +1048,14 @@ var ProvidersAppLovinSdk = (function () {
|
|
|
904
1048
|
}
|
|
905
1049
|
};
|
|
906
1050
|
|
|
907
|
-
const
|
|
1051
|
+
const getString = (kv, key) => {
|
|
1052
|
+
const value = kv === null || kv === void 0 ? void 0 : kv[key];
|
|
1053
|
+
return typeof value === 'string' && value ? value : undefined;
|
|
1054
|
+
};
|
|
1055
|
+
const tag = ({ data, eventName, eventId, manifestVariables, userId, destination, getEdgeData, }) => {
|
|
1056
|
+
var _a, _b;
|
|
908
1057
|
const payload = {
|
|
909
|
-
sdkVersion: "1.
|
|
1058
|
+
sdkVersion: "1.72.0" ,
|
|
910
1059
|
};
|
|
911
1060
|
if (window.axon && manifestVariables['enableBrowser'] === '1') {
|
|
912
1061
|
try {
|
|
@@ -915,28 +1064,68 @@ var ProvidersAppLovinSdk = (function () {
|
|
|
915
1064
|
// email/phone usually arrive via the data API (stored in session KV),
|
|
916
1065
|
// not on the event payload itself.
|
|
917
1066
|
const kv = getData(destination, 'session')['kv'];
|
|
1067
|
+
// Same identity set the CAPI attaches server-side, so the browser pixel
|
|
1068
|
+
// is not the weaker of the two paths on a shopper we already know.
|
|
918
1069
|
const userData = { user_id: userId };
|
|
919
|
-
|
|
920
|
-
|
|
1070
|
+
// Checked per source rather than after the `||`, so a non-string on the
|
|
1071
|
+
// payload cannot displace a valid string in KV.
|
|
1072
|
+
const email = (_a = getString(data, 'email')) !== null && _a !== void 0 ? _a : getString(kv, 'email');
|
|
1073
|
+
if (email) {
|
|
921
1074
|
userData.email = email;
|
|
922
1075
|
}
|
|
923
|
-
const phone = (
|
|
924
|
-
if (phone
|
|
1076
|
+
const phone = (_b = getString(data, 'phone')) !== null && _b !== void 0 ? _b : getString(kv, 'phone');
|
|
1077
|
+
if (phone) {
|
|
925
1078
|
userData.phone = phone;
|
|
926
1079
|
}
|
|
927
|
-
const
|
|
928
|
-
|
|
929
|
-
userData
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1080
|
+
const zip = getString(kv, 'zip');
|
|
1081
|
+
if (zip) {
|
|
1082
|
+
userData.zip = zip;
|
|
1083
|
+
}
|
|
1084
|
+
const country = getString(kv, 'country');
|
|
1085
|
+
if (country) {
|
|
1086
|
+
userData.country_code = country;
|
|
1087
|
+
}
|
|
1088
|
+
const send = (isNewCustomer) => {
|
|
1089
|
+
var _a;
|
|
1090
|
+
const event = getEventData({
|
|
1091
|
+
eventName,
|
|
1092
|
+
userData,
|
|
1093
|
+
data,
|
|
1094
|
+
defaultCategory,
|
|
1095
|
+
categoryMapping,
|
|
1096
|
+
userId,
|
|
1097
|
+
isNewCustomer,
|
|
1098
|
+
});
|
|
1099
|
+
if (!event) {
|
|
1100
|
+
logger.log(`[${packageName}] ${eventName} not supported`);
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
(_a = window.axon) === null || _a === void 0 ? void 0 : _a.call(window, 'track', event.name, event.data, { dedupe_id: eventId });
|
|
1104
|
+
};
|
|
1105
|
+
if (eventName !== 'Purchase') {
|
|
1106
|
+
send();
|
|
1107
|
+
return payload;
|
|
1108
|
+
}
|
|
1109
|
+
// Prefer the flag already saved in the browser (the Shopify pixel's
|
|
1110
|
+
// `data` call writes it just before Purchase). Only query the edge when
|
|
1111
|
+
// the browser value is missing.
|
|
1112
|
+
const browserValue = destination
|
|
1113
|
+
? getIsNewCustomerFromBrowser(destination)
|
|
1114
|
+
: undefined;
|
|
1115
|
+
if (typeof browserValue === 'boolean') {
|
|
1116
|
+
send(browserValue);
|
|
1117
|
+
}
|
|
1118
|
+
else if (getEdgeData) {
|
|
1119
|
+
getIsNewCustomerFlag(getEdgeData)
|
|
1120
|
+
// The fallback is the rejection handler rather than a trailing
|
|
1121
|
+
// `catch`, so only a failed lookup sends without the flag; a `send`
|
|
1122
|
+
// that throws must not be retried into a duplicate pixel call.
|
|
1123
|
+
.then(send, () => send())
|
|
1124
|
+
.catch((error) => logger.error(error));
|
|
1125
|
+
}
|
|
1126
|
+
else {
|
|
1127
|
+
send();
|
|
938
1128
|
}
|
|
939
|
-
window.axon('track', event.name, event.data, { dedupe_id: eventId });
|
|
940
1129
|
}
|
|
941
1130
|
catch (e) {
|
|
942
1131
|
logger.error(e);
|
package/index.mjs
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AppLovin's `user_data.zip` is documented as "The user's billing zip code. Use
|
|
3
|
+
* only the first 5 digits for US zip codes", so a ZIP+4 arriving from a billing
|
|
4
|
+
* address is truncated to its first five digits.
|
|
5
|
+
*
|
|
6
|
+
* Only for US values. A blind five-character slice would corrupt every other
|
|
7
|
+
* postal format — UK `SW1A 1AA` becomes `SW1A `, Canadian `K1A 0B1` becomes
|
|
8
|
+
* `K1A 0` — so anything else passes through as given.
|
|
9
|
+
*
|
|
10
|
+
* Shared by the cdn and sdk paths so both send the same value.
|
|
11
|
+
*/
|
|
12
|
+
const getNormalizedZip = (zip, country) => {
|
|
13
|
+
var _a;
|
|
14
|
+
const trimmed = zip === null || zip === void 0 ? void 0 : zip.trim();
|
|
15
|
+
if (!trimmed) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
// ISO 3166 per the same spec; `USA` is accepted because billing addresses
|
|
19
|
+
// supply both spellings.
|
|
20
|
+
const isUS = ['US', 'USA'].includes((_a = country === null || country === void 0 ? void 0 : country.trim().toUpperCase()) !== null && _a !== void 0 ? _a : '');
|
|
21
|
+
// The country is optional on both call paths, and the ZIP+4 shape belongs to
|
|
22
|
+
// no other postal format, so a value in that shape is truncated even when no
|
|
23
|
+
// country accompanies it.
|
|
24
|
+
const isZipPlusFour = /^\d{5}-?\d{4}$/.test(trimmed);
|
|
25
|
+
if (!isUS && !isZipPlusFour) {
|
|
26
|
+
return trimmed;
|
|
27
|
+
}
|
|
28
|
+
return trimmed.replace(/\D/g, '').slice(0, 5) || undefined;
|
|
29
|
+
};
|
|
30
|
+
|
|
1
31
|
const packageName = 'appLovin';
|
|
2
32
|
|
|
3
33
|
const t = [
|
|
@@ -119,6 +149,20 @@ const getSession = (key) => {
|
|
|
119
149
|
}
|
|
120
150
|
};
|
|
121
151
|
|
|
152
|
+
const getIsNewCustomerFlag = async (getData) => {
|
|
153
|
+
const { isNewCustomer } = await new Promise((resolve) => getData(['isNewCustomer'], resolve));
|
|
154
|
+
return isNewCustomer ? isNewCustomer === 'true' : undefined;
|
|
155
|
+
};
|
|
156
|
+
// Reads the isNewCustomer flag already persisted in the browser's session KV
|
|
157
|
+
// (written by the `data` API call, which runs before the Purchase event). Lets
|
|
158
|
+
// callers use the local value and avoid a redundant edge round-trip; returns
|
|
159
|
+
// undefined when the value has not been stored yet so the caller can fall back.
|
|
160
|
+
const getIsNewCustomerFromBrowser = (destination) => {
|
|
161
|
+
const kv = getData(destination, 'session')['kv'];
|
|
162
|
+
const isNewCustomer = kv === null || kv === void 0 ? void 0 : kv['isNewCustomer'];
|
|
163
|
+
return isNewCustomer ? isNewCustomer === 'true' : undefined;
|
|
164
|
+
};
|
|
165
|
+
|
|
122
166
|
/**
|
|
123
167
|
* Known ad-network click ID query parameters and the human-readable provider
|
|
124
168
|
* label each one maps to. Used by the CDN worker to resolve `inSessionTouch`
|
|
@@ -156,6 +200,18 @@ const CLICK_IDS = [
|
|
|
156
200
|
{ param: 'duel_a', label: 'Duel' }, // Duel referral/advocacy
|
|
157
201
|
];
|
|
158
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Checks whether a value is a plain JavaScript object (created via `{}` or `new Object()`).
|
|
205
|
+
* Returns false for arrays, class instances, null, and objects with modified prototypes.
|
|
206
|
+
* Note: Objects created with `Object.create(null)` will return false.
|
|
207
|
+
*/
|
|
208
|
+
const isPlainObject = (value) => {
|
|
209
|
+
if (typeof value !== 'object' || value === null) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
return Object.getPrototypeOf(value) === Object.prototype;
|
|
213
|
+
};
|
|
214
|
+
|
|
159
215
|
const expand = (str) => str.split(',').flatMap((entry) => {
|
|
160
216
|
if (!entry.includes('-')) {
|
|
161
217
|
return entry;
|
|
@@ -221,6 +277,18 @@ const normalizeEmail = (email) => {
|
|
|
221
277
|
return trimmedEmail;
|
|
222
278
|
};
|
|
223
279
|
|
|
280
|
+
function tryParse(json, fallbackValue) {
|
|
281
|
+
try {
|
|
282
|
+
if (json) {
|
|
283
|
+
return JSON.parse(json);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
/* do nothing */
|
|
288
|
+
}
|
|
289
|
+
return fallbackValue;
|
|
290
|
+
}
|
|
291
|
+
|
|
224
292
|
/**
|
|
225
293
|
* ISO-3166 2-leter country codes and their names
|
|
226
294
|
* @see https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
|
|
@@ -567,6 +635,78 @@ new Set([
|
|
|
567
635
|
...caProvinces.keys(),
|
|
568
636
|
]);
|
|
569
637
|
|
|
638
|
+
/**
|
|
639
|
+
* Self-describing JSON compaction codec (`packMappings` / `unpackMappings`).
|
|
640
|
+
*
|
|
641
|
+
* Worker variables have a 5KB-per-variable limit, and objects waste most of it
|
|
642
|
+
* repeating long field names on every row. `packMappings` rewrites every object
|
|
643
|
+
* key (at any depth) to a short auto-derived token and carries a single embedded
|
|
644
|
+
* legend, so the long name is stored once. `unpackMappings` reverses it using
|
|
645
|
+
* that legend alone — there is NO external key-map to maintain per provider.
|
|
646
|
+
*
|
|
647
|
+
* Works on ANY JSON value: a mapping array (the common case), a single plain
|
|
648
|
+
* object, nested structures, or primitives all round-trip.
|
|
649
|
+
*
|
|
650
|
+
* The wire format is a versioned envelope:
|
|
651
|
+
* { "~": 1, "k": { <short>: <long>, ... }, "d": <transformed value> }
|
|
652
|
+
*
|
|
653
|
+
* Guarantees (all covered by tests):
|
|
654
|
+
* - `unpackMappings(packMappings(x))` deep-equals `x` with empty fields removed,
|
|
655
|
+
* for any JSON value (objects, arrays, nesting, primitives).
|
|
656
|
+
* - Short keys are always unique — long names sharing a prefix are disambiguated
|
|
657
|
+
* automatically, so `packMappings` never loses or merges a field.
|
|
658
|
+
* - `unpackMappings` returns legacy plain JSON (no envelope) unchanged, so values
|
|
659
|
+
* written before compaction keep decoding; invalid/empty input → `undefined`.
|
|
660
|
+
*
|
|
661
|
+
* The building blocks (`isEmptyValue`, `deriveShortKey`, `encodeMappings`,
|
|
662
|
+
* `decodeMappings`, `isMappingEnvelope`) are exported as pure functions so each
|
|
663
|
+
* is independently testable.
|
|
664
|
+
*/
|
|
665
|
+
const CODEC_VERSION = 1;
|
|
666
|
+
const VERSION_KEY = '~';
|
|
667
|
+
const LEGEND_KEY = 'k';
|
|
668
|
+
const DATA_KEY = 'd';
|
|
669
|
+
/**
|
|
670
|
+
* Pure inverse of `encodeMappings`: rewrites every object key back to its long
|
|
671
|
+
* name via the legend (unknown keys pass through). Arrays and primitives pass
|
|
672
|
+
* through structurally.
|
|
673
|
+
*/
|
|
674
|
+
const decodeMappings = (data, legend) => {
|
|
675
|
+
var _a;
|
|
676
|
+
if (Array.isArray(data)) {
|
|
677
|
+
return data.map((item) => decodeMappings(item, legend));
|
|
678
|
+
}
|
|
679
|
+
if (isPlainObject(data)) {
|
|
680
|
+
const result = {};
|
|
681
|
+
for (const [key, val] of Object.entries(data)) {
|
|
682
|
+
result[(_a = legend[key]) !== null && _a !== void 0 ? _a : key] = decodeMappings(val, legend);
|
|
683
|
+
}
|
|
684
|
+
return result;
|
|
685
|
+
}
|
|
686
|
+
return data;
|
|
687
|
+
};
|
|
688
|
+
/** True when a parsed value is a `packMappings` envelope (vs. legacy plain JSON). */
|
|
689
|
+
const isMappingEnvelope = (value) => isPlainObject(value) &&
|
|
690
|
+
value[VERSION_KEY] === CODEC_VERSION &&
|
|
691
|
+
isPlainObject(value[LEGEND_KEY]) &&
|
|
692
|
+
DATA_KEY in value;
|
|
693
|
+
function unpackMappings(text, fallback) {
|
|
694
|
+
const parsed = tryParse(text);
|
|
695
|
+
if (parsed === undefined || parsed === null) {
|
|
696
|
+
return fallback;
|
|
697
|
+
}
|
|
698
|
+
if (isMappingEnvelope(parsed)) {
|
|
699
|
+
return decodeMappings(parsed[DATA_KEY], parsed[LEGEND_KEY]);
|
|
700
|
+
}
|
|
701
|
+
// A versioned envelope this reader can't decode (e.g. a future CODEC_VERSION
|
|
702
|
+
// written by a newer UI while this worker still lags). Returning the raw
|
|
703
|
+
// `{~,k,d}` object would be silent corruption, so fall back instead.
|
|
704
|
+
if (isPlainObject(parsed) && VERSION_KEY in parsed) {
|
|
705
|
+
return fallback;
|
|
706
|
+
}
|
|
707
|
+
return parsed;
|
|
708
|
+
}
|
|
709
|
+
|
|
570
710
|
/**
|
|
571
711
|
* Exact utm_source normalization (lowercase key → canonical name).
|
|
572
712
|
*
|
|
@@ -689,24 +829,17 @@ new Set([
|
|
|
689
829
|
]);
|
|
690
830
|
|
|
691
831
|
// eslint-disable-next-line @nx/enforce-module-boundaries
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
logger.error(`Error parsing category: ${category}`, error);
|
|
699
|
-
return undefined;
|
|
700
|
-
}
|
|
701
|
-
};
|
|
832
|
+
// The same helpers as the cdn copy in cdn/src/lib/utils.ts, so a malformed
|
|
833
|
+
// mapping fails identically on both sides. `unpackMappings` yields the fallback
|
|
834
|
+
// for input that parses to `null`, and the array guard covers valid JSON that
|
|
835
|
+
// is not an array; either would otherwise reach `.map` and throw, which in
|
|
836
|
+
// browser SDK code takes the whole tag down.
|
|
837
|
+
const getParsedCategoryId = (category) => { var _a; return (_a = tryParse(category)) === null || _a === void 0 ? void 0 : _a.value; };
|
|
702
838
|
const getCategories = (categoryMapping) => {
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
categoryMappings = JSON.parse(categoryMapping || '[]');
|
|
706
|
-
}
|
|
707
|
-
catch {
|
|
839
|
+
const categoryMappings = unpackMappings(categoryMapping, []);
|
|
840
|
+
if (!Array.isArray(categoryMappings)) {
|
|
708
841
|
logger.error('Unable to parse category mappings');
|
|
709
|
-
|
|
842
|
+
return new Map();
|
|
710
843
|
}
|
|
711
844
|
return new Map(categoryMappings.map(({ storeCategory, appLovinCategory }) => [
|
|
712
845
|
storeCategory.toLowerCase(),
|
|
@@ -743,6 +876,13 @@ const getEventUserData = (userId, data) => {
|
|
|
743
876
|
userData.phone = normalizedPhone;
|
|
744
877
|
}
|
|
745
878
|
}
|
|
879
|
+
const zip = getNormalizedZip(data.zip, data.country_code);
|
|
880
|
+
if (zip) {
|
|
881
|
+
userData.zip = zip;
|
|
882
|
+
}
|
|
883
|
+
if (data.country_code) {
|
|
884
|
+
userData.country_code = data.country_code;
|
|
885
|
+
}
|
|
746
886
|
return userData;
|
|
747
887
|
};
|
|
748
888
|
/**
|
|
@@ -754,9 +894,10 @@ const getEventUserData = (userId, data) => {
|
|
|
754
894
|
* @param defaultCategory - JSON string containing default category configuration with label and value
|
|
755
895
|
* @param categoryMapping - JSON string containing array of category mappings
|
|
756
896
|
* @param userId - The explicit user ID to use in event user_data
|
|
897
|
+
* @param isNewCustomer - New/returning buyer flag, applied to Purchase only
|
|
757
898
|
* @returns EventData payload if the event is supported, undefined otherwise
|
|
758
899
|
*/
|
|
759
|
-
const getEventData = ({ eventName, userData, data, defaultCategory, categoryMapping, userId, }) => {
|
|
900
|
+
const getEventData = ({ eventName, userData, data, defaultCategory, categoryMapping, userId, isNewCustomer, }) => {
|
|
760
901
|
var _a, _b, _c;
|
|
761
902
|
const categories = getCategories(categoryMapping);
|
|
762
903
|
const defaultCategoryId = getParsedCategoryId(defaultCategory);
|
|
@@ -855,6 +996,9 @@ const getEventData = ({ eventName, userData, data, defaultCategory, categoryMapp
|
|
|
855
996
|
shipping: (_b = data.shippingCost) !== null && _b !== void 0 ? _b : 0,
|
|
856
997
|
tax: (_c = data.taxCost) !== null && _c !== void 0 ? _c : 0,
|
|
857
998
|
user_data: getEventUserData(userId, userData),
|
|
999
|
+
...(typeof isNewCustomer === 'boolean' && {
|
|
1000
|
+
measurement: { is_new_customer: isNewCustomer },
|
|
1001
|
+
}),
|
|
858
1002
|
},
|
|
859
1003
|
};
|
|
860
1004
|
}
|
|
@@ -901,9 +1045,14 @@ const getEventData = ({ eventName, userData, data, defaultCategory, categoryMapp
|
|
|
901
1045
|
}
|
|
902
1046
|
};
|
|
903
1047
|
|
|
904
|
-
const
|
|
1048
|
+
const getString = (kv, key) => {
|
|
1049
|
+
const value = kv === null || kv === void 0 ? void 0 : kv[key];
|
|
1050
|
+
return typeof value === 'string' && value ? value : undefined;
|
|
1051
|
+
};
|
|
1052
|
+
const tag = ({ data, eventName, eventId, manifestVariables, userId, destination, getEdgeData, }) => {
|
|
1053
|
+
var _a, _b;
|
|
905
1054
|
const payload = {
|
|
906
|
-
sdkVersion: "1.
|
|
1055
|
+
sdkVersion: "1.72.0" ,
|
|
907
1056
|
};
|
|
908
1057
|
if (window.axon && manifestVariables['enableBrowser'] === '1') {
|
|
909
1058
|
try {
|
|
@@ -912,28 +1061,68 @@ const tag = ({ data, eventName, eventId, manifestVariables, userId, destination,
|
|
|
912
1061
|
// email/phone usually arrive via the data API (stored in session KV),
|
|
913
1062
|
// not on the event payload itself.
|
|
914
1063
|
const kv = getData(destination, 'session')['kv'];
|
|
1064
|
+
// Same identity set the CAPI attaches server-side, so the browser pixel
|
|
1065
|
+
// is not the weaker of the two paths on a shopper we already know.
|
|
915
1066
|
const userData = { user_id: userId };
|
|
916
|
-
|
|
917
|
-
|
|
1067
|
+
// Checked per source rather than after the `||`, so a non-string on the
|
|
1068
|
+
// payload cannot displace a valid string in KV.
|
|
1069
|
+
const email = (_a = getString(data, 'email')) !== null && _a !== void 0 ? _a : getString(kv, 'email');
|
|
1070
|
+
if (email) {
|
|
918
1071
|
userData.email = email;
|
|
919
1072
|
}
|
|
920
|
-
const phone = (
|
|
921
|
-
if (phone
|
|
1073
|
+
const phone = (_b = getString(data, 'phone')) !== null && _b !== void 0 ? _b : getString(kv, 'phone');
|
|
1074
|
+
if (phone) {
|
|
922
1075
|
userData.phone = phone;
|
|
923
1076
|
}
|
|
924
|
-
const
|
|
925
|
-
|
|
926
|
-
userData
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
1077
|
+
const zip = getString(kv, 'zip');
|
|
1078
|
+
if (zip) {
|
|
1079
|
+
userData.zip = zip;
|
|
1080
|
+
}
|
|
1081
|
+
const country = getString(kv, 'country');
|
|
1082
|
+
if (country) {
|
|
1083
|
+
userData.country_code = country;
|
|
1084
|
+
}
|
|
1085
|
+
const send = (isNewCustomer) => {
|
|
1086
|
+
var _a;
|
|
1087
|
+
const event = getEventData({
|
|
1088
|
+
eventName,
|
|
1089
|
+
userData,
|
|
1090
|
+
data,
|
|
1091
|
+
defaultCategory,
|
|
1092
|
+
categoryMapping,
|
|
1093
|
+
userId,
|
|
1094
|
+
isNewCustomer,
|
|
1095
|
+
});
|
|
1096
|
+
if (!event) {
|
|
1097
|
+
logger.log(`[${packageName}] ${eventName} not supported`);
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
(_a = window.axon) === null || _a === void 0 ? void 0 : _a.call(window, 'track', event.name, event.data, { dedupe_id: eventId });
|
|
1101
|
+
};
|
|
1102
|
+
if (eventName !== 'Purchase') {
|
|
1103
|
+
send();
|
|
1104
|
+
return payload;
|
|
1105
|
+
}
|
|
1106
|
+
// Prefer the flag already saved in the browser (the Shopify pixel's
|
|
1107
|
+
// `data` call writes it just before Purchase). Only query the edge when
|
|
1108
|
+
// the browser value is missing.
|
|
1109
|
+
const browserValue = destination
|
|
1110
|
+
? getIsNewCustomerFromBrowser(destination)
|
|
1111
|
+
: undefined;
|
|
1112
|
+
if (typeof browserValue === 'boolean') {
|
|
1113
|
+
send(browserValue);
|
|
1114
|
+
}
|
|
1115
|
+
else if (getEdgeData) {
|
|
1116
|
+
getIsNewCustomerFlag(getEdgeData)
|
|
1117
|
+
// The fallback is the rejection handler rather than a trailing
|
|
1118
|
+
// `catch`, so only a failed lookup sends without the flag; a `send`
|
|
1119
|
+
// that throws must not be retried into a duplicate pixel call.
|
|
1120
|
+
.then(send, () => send())
|
|
1121
|
+
.catch((error) => logger.error(error));
|
|
1122
|
+
}
|
|
1123
|
+
else {
|
|
1124
|
+
send();
|
|
935
1125
|
}
|
|
936
|
-
window.axon('track', event.name, event.data, { dedupe_id: eventId });
|
|
937
1126
|
}
|
|
938
1127
|
catch (e) {
|
|
939
1128
|
logger.error(e);
|