@yesplz-ai/analytics-react-native 0.1.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/README.md +183 -0
- package/dist/index.cjs +749 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +130 -0
- package/dist/index.d.ts +130 -0
- package/dist/index.js +707 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
Analytics: () => Analytics,
|
|
34
|
+
EventType: () => EventType,
|
|
35
|
+
ItemListId: () => ItemListId,
|
|
36
|
+
VERSION: () => VERSION,
|
|
37
|
+
analytics: () => analytics,
|
|
38
|
+
normalizeItemListId: () => normalizeItemListId
|
|
39
|
+
});
|
|
40
|
+
module.exports = __toCommonJS(index_exports);
|
|
41
|
+
|
|
42
|
+
// src/analytics.ts
|
|
43
|
+
var import_react_native = require("react-native");
|
|
44
|
+
|
|
45
|
+
// src/constants.ts
|
|
46
|
+
var VERSION = "0.1.0-rn";
|
|
47
|
+
var EventType = {
|
|
48
|
+
ViewItemList: "view_item_list",
|
|
49
|
+
ViewItem: "yp_product_view",
|
|
50
|
+
ViewProduct: "yp_product_view",
|
|
51
|
+
SelectItem: "yp_product_select",
|
|
52
|
+
SelectProduct: "yp_product_select",
|
|
53
|
+
AddToCart: "yp_product_cart",
|
|
54
|
+
RemoveFromCart: "yp_product_remove_from_cart",
|
|
55
|
+
BeginCheckout: "yp_begin_checkout",
|
|
56
|
+
Purchase: "yp_product_purchase",
|
|
57
|
+
FavoriteProduct: "yp_product_favorite",
|
|
58
|
+
UnfavoriteProduct: "yp_product_unfavorite",
|
|
59
|
+
AddToWishlist: "yp_product_favorite",
|
|
60
|
+
RemoveFromWishlist: "yp_product_unfavorite",
|
|
61
|
+
ViewBrand: "yp_brand_view",
|
|
62
|
+
FavoriteBrand: "yp_brand_favorite",
|
|
63
|
+
UnfavoriteBrand: "yp_brand_unfavorite"
|
|
64
|
+
};
|
|
65
|
+
var GaEventType = {
|
|
66
|
+
[EventType.ViewProduct]: "view_item",
|
|
67
|
+
[EventType.SelectItem]: "select_item",
|
|
68
|
+
[EventType.AddToCart]: "add_to_cart",
|
|
69
|
+
[EventType.RemoveFromCart]: "remove_from_cart",
|
|
70
|
+
[EventType.BeginCheckout]: "begin_checkout",
|
|
71
|
+
[EventType.Purchase]: "purchase",
|
|
72
|
+
[EventType.FavoriteProduct]: "add_to_wishlist",
|
|
73
|
+
[EventType.UnfavoriteProduct]: "remove_from_wishlist"
|
|
74
|
+
};
|
|
75
|
+
var GaEventTypeReverse = {
|
|
76
|
+
view_item: EventType.ViewProduct,
|
|
77
|
+
select_item: EventType.SelectItem,
|
|
78
|
+
add_to_cart: EventType.AddToCart,
|
|
79
|
+
remove_from_cart: EventType.RemoveFromCart,
|
|
80
|
+
begin_checkout: EventType.BeginCheckout,
|
|
81
|
+
purchase: EventType.Purchase,
|
|
82
|
+
add_to_wishlist: EventType.FavoriteProduct,
|
|
83
|
+
remove_from_wishlist: EventType.UnfavoriteProduct
|
|
84
|
+
};
|
|
85
|
+
var ItemListId = {
|
|
86
|
+
SEARCH_RESULTS: "search-results",
|
|
87
|
+
PRODUCT_LIST: "product-list",
|
|
88
|
+
YOU_MAY_ALSO_LIKE: "you-may-also-like",
|
|
89
|
+
COMPLETE_THE_LOOK: "complete-the-look",
|
|
90
|
+
COLLABORATIVE_FILTERING: "collaborative-filtering",
|
|
91
|
+
MORE_FROM_BRAND: "more-from-brand",
|
|
92
|
+
STYLE_WITH: "style-with",
|
|
93
|
+
SHOPPING_CART: "shopping-cart",
|
|
94
|
+
ITEM_FOR_YOU: "item-for-you",
|
|
95
|
+
BRAND_FOR_YOU: "brand-for-you"
|
|
96
|
+
};
|
|
97
|
+
var LIST_ID_ALIASES = {
|
|
98
|
+
Search: ItemListId.SEARCH_RESULTS,
|
|
99
|
+
"search-results": ItemListId.SEARCH_RESULTS,
|
|
100
|
+
PLP: ItemListId.PRODUCT_LIST,
|
|
101
|
+
"product-list": ItemListId.PRODUCT_LIST,
|
|
102
|
+
YMAL: ItemListId.YOU_MAY_ALSO_LIKE,
|
|
103
|
+
"you-may-also-like": ItemListId.YOU_MAY_ALSO_LIKE,
|
|
104
|
+
CTL: ItemListId.COMPLETE_THE_LOOK,
|
|
105
|
+
"complete-the-look": ItemListId.COMPLETE_THE_LOOK,
|
|
106
|
+
CF: ItemListId.COLLABORATIVE_FILTERING,
|
|
107
|
+
"collaborative-filtering": ItemListId.COLLABORATIVE_FILTERING,
|
|
108
|
+
"frequently-bought-together": ItemListId.COLLABORATIVE_FILTERING,
|
|
109
|
+
Brand: ItemListId.MORE_FROM_BRAND,
|
|
110
|
+
"more-from-brand": ItemListId.MORE_FROM_BRAND,
|
|
111
|
+
STW: ItemListId.STYLE_WITH,
|
|
112
|
+
"style-with": ItemListId.STYLE_WITH,
|
|
113
|
+
CART: ItemListId.SHOPPING_CART,
|
|
114
|
+
"shopping-cart": ItemListId.SHOPPING_CART,
|
|
115
|
+
"item-for-you": ItemListId.ITEM_FOR_YOU,
|
|
116
|
+
"brand-for-you": ItemListId.BRAND_FOR_YOU
|
|
117
|
+
};
|
|
118
|
+
function normalizeItemListId(input) {
|
|
119
|
+
if (!input) return input;
|
|
120
|
+
const alias = LIST_ID_ALIASES[input];
|
|
121
|
+
if (alias) return alias;
|
|
122
|
+
const canonical = Object.values(ItemListId);
|
|
123
|
+
if (canonical.includes(input)) return input;
|
|
124
|
+
return input;
|
|
125
|
+
}
|
|
126
|
+
var API_EVENTS = [
|
|
127
|
+
EventType.ViewProduct,
|
|
128
|
+
EventType.AddToCart,
|
|
129
|
+
EventType.RemoveFromCart,
|
|
130
|
+
EventType.BeginCheckout,
|
|
131
|
+
EventType.Purchase,
|
|
132
|
+
EventType.FavoriteProduct,
|
|
133
|
+
EventType.UnfavoriteProduct,
|
|
134
|
+
EventType.ViewBrand,
|
|
135
|
+
EventType.FavoriteBrand,
|
|
136
|
+
EventType.UnfavoriteBrand
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
// src/storage.ts
|
|
140
|
+
var import_async_storage = __toESM(require("@react-native-async-storage/async-storage"), 1);
|
|
141
|
+
var StorageKeys = {
|
|
142
|
+
CID: "yp_cid",
|
|
143
|
+
SESSION: "yp_session",
|
|
144
|
+
USER_ID: "yp_user_id",
|
|
145
|
+
USER_PROPS: "yp_user_props",
|
|
146
|
+
ATTRIBUTION: "yp_attribution",
|
|
147
|
+
PENDING_EVENTS: "yp_pending_events"
|
|
148
|
+
};
|
|
149
|
+
var Storage = {
|
|
150
|
+
async getItem(key) {
|
|
151
|
+
try {
|
|
152
|
+
return await import_async_storage.default.getItem(key);
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
async setItem(key, value) {
|
|
158
|
+
try {
|
|
159
|
+
await import_async_storage.default.setItem(key, value);
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
async removeItem(key) {
|
|
164
|
+
try {
|
|
165
|
+
await import_async_storage.default.removeItem(key);
|
|
166
|
+
} catch {
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
async getJSON(key, fallback) {
|
|
170
|
+
const raw = await Storage.getItem(key);
|
|
171
|
+
if (!raw) return fallback;
|
|
172
|
+
try {
|
|
173
|
+
return JSON.parse(raw);
|
|
174
|
+
} catch {
|
|
175
|
+
return fallback;
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
async setJSON(key, value) {
|
|
179
|
+
try {
|
|
180
|
+
await Storage.setItem(key, JSON.stringify(value));
|
|
181
|
+
} catch {
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// src/ids.ts
|
|
187
|
+
function generateId() {
|
|
188
|
+
return Date.now().toString(36) + Math.random().toString(36).substring(2);
|
|
189
|
+
}
|
|
190
|
+
function hashCode(str) {
|
|
191
|
+
let hash = 0;
|
|
192
|
+
for (let i = 0; i < str.length; i++) {
|
|
193
|
+
hash = (hash << 5) - hash + str.charCodeAt(i);
|
|
194
|
+
hash |= 0;
|
|
195
|
+
}
|
|
196
|
+
return hash;
|
|
197
|
+
}
|
|
198
|
+
function numericSessionId(sessionId) {
|
|
199
|
+
if (!sessionId) return Math.floor(Date.now() / 1e3);
|
|
200
|
+
return Math.abs(hashCode(sessionId)) || Math.floor(Date.now() / 1e3);
|
|
201
|
+
}
|
|
202
|
+
async function loadIdentity() {
|
|
203
|
+
const [cidStored, sessionStored, userIdStored, userPropsStored] = await Promise.all([
|
|
204
|
+
Storage.getItem(StorageKeys.CID),
|
|
205
|
+
Storage.getItem(StorageKeys.SESSION),
|
|
206
|
+
Storage.getItem(StorageKeys.USER_ID),
|
|
207
|
+
Storage.getItem(StorageKeys.USER_PROPS)
|
|
208
|
+
]);
|
|
209
|
+
let cid = cidStored;
|
|
210
|
+
if (!cid) {
|
|
211
|
+
cid = generateId();
|
|
212
|
+
await Storage.setItem(StorageKeys.CID, cid);
|
|
213
|
+
}
|
|
214
|
+
let sessionId = sessionStored;
|
|
215
|
+
if (!sessionId) {
|
|
216
|
+
sessionId = generateId();
|
|
217
|
+
await Storage.setItem(StorageKeys.SESSION, sessionId);
|
|
218
|
+
}
|
|
219
|
+
let userProperties = {};
|
|
220
|
+
if (userPropsStored) {
|
|
221
|
+
try {
|
|
222
|
+
userProperties = JSON.parse(userPropsStored);
|
|
223
|
+
} catch {
|
|
224
|
+
userProperties = {};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return { cid, sessionId, userId: userIdStored, userProperties };
|
|
228
|
+
}
|
|
229
|
+
async function saveSession(sessionId) {
|
|
230
|
+
await Storage.setItem(StorageKeys.SESSION, sessionId);
|
|
231
|
+
}
|
|
232
|
+
async function saveUser(userId, userProperties, cacheUserId) {
|
|
233
|
+
if (cacheUserId) {
|
|
234
|
+
await Storage.setItem(StorageKeys.USER_ID, userId);
|
|
235
|
+
}
|
|
236
|
+
await Storage.setJSON(StorageKeys.USER_PROPS, userProperties);
|
|
237
|
+
}
|
|
238
|
+
async function clearUser() {
|
|
239
|
+
await Storage.removeItem(StorageKeys.USER_ID);
|
|
240
|
+
await Storage.removeItem(StorageKeys.USER_PROPS);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/attribution.ts
|
|
244
|
+
var EXPIRY_MS = 30 * 60 * 1e3;
|
|
245
|
+
function normalizeId(rawId, idPrefix) {
|
|
246
|
+
if (!rawId) return null;
|
|
247
|
+
const idStr = String(rawId);
|
|
248
|
+
if (idPrefix && idStr.startsWith(idPrefix)) {
|
|
249
|
+
return idStr.slice(idPrefix.length);
|
|
250
|
+
}
|
|
251
|
+
const match = idStr.match(/^[a-zA-Z]+-(.+)$/);
|
|
252
|
+
return match ? match[1] : idStr;
|
|
253
|
+
}
|
|
254
|
+
async function loadStore() {
|
|
255
|
+
return Storage.getJSON(StorageKeys.ATTRIBUTION, {});
|
|
256
|
+
}
|
|
257
|
+
async function saveStore(store) {
|
|
258
|
+
await Storage.setJSON(StorageKeys.ATTRIBUTION, store);
|
|
259
|
+
}
|
|
260
|
+
function pruneExpired(store) {
|
|
261
|
+
const now = Date.now();
|
|
262
|
+
const next = {};
|
|
263
|
+
for (const [k, v] of Object.entries(store)) {
|
|
264
|
+
if (now - v.ts <= EXPIRY_MS) next[k] = v;
|
|
265
|
+
}
|
|
266
|
+
return next;
|
|
267
|
+
}
|
|
268
|
+
async function handleAttribution(eventType, props, idPrefix) {
|
|
269
|
+
const normalizedEventType = GaEventTypeReverse[eventType] || eventType;
|
|
270
|
+
if (normalizedEventType === EventType.SelectItem) {
|
|
271
|
+
const itemListId = props.item_list_id;
|
|
272
|
+
const rawId = props.id ?? props.items?.[0]?.item_id;
|
|
273
|
+
const productId = normalizeId(rawId, idPrefix);
|
|
274
|
+
if (itemListId && productId) {
|
|
275
|
+
let store = await loadStore();
|
|
276
|
+
store = pruneExpired(store);
|
|
277
|
+
store[productId] = { source: itemListId, ts: Date.now() };
|
|
278
|
+
await saveStore(store);
|
|
279
|
+
}
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (normalizedEventType === EventType.ViewProduct) {
|
|
283
|
+
const rawId = props.id ?? props.items?.[0]?.item_id;
|
|
284
|
+
const productId = normalizeId(rawId, idPrefix);
|
|
285
|
+
if (!productId) return;
|
|
286
|
+
const store = await loadStore();
|
|
287
|
+
const entry = store[productId];
|
|
288
|
+
if (entry && Date.now() - entry.ts <= EXPIRY_MS) {
|
|
289
|
+
props.item_list_id = entry.source;
|
|
290
|
+
if (Array.isArray(props.items)) {
|
|
291
|
+
props.items = props.items.map((item) => ({ ...item, item_list_id: entry.source }));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (normalizedEventType === EventType.AddToCart) {
|
|
297
|
+
if (!Array.isArray(props.items)) return;
|
|
298
|
+
const store = await loadStore();
|
|
299
|
+
props.items = props.items.map((item) => {
|
|
300
|
+
const productId = normalizeId(item.item_id, idPrefix);
|
|
301
|
+
const entry = productId ? store[productId] : null;
|
|
302
|
+
if (entry && Date.now() - entry.ts <= EXPIRY_MS) {
|
|
303
|
+
return { ...item, item_list_id: entry.source };
|
|
304
|
+
}
|
|
305
|
+
return item;
|
|
306
|
+
});
|
|
307
|
+
const firstMatch = props.items.find((item) => item.item_list_id);
|
|
308
|
+
if (firstMatch?.item_list_id) {
|
|
309
|
+
props.item_list_id = firstMatch.item_list_id;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/ga.ts
|
|
315
|
+
function toSnakeCase(str) {
|
|
316
|
+
return str.replace(/-/g, "_");
|
|
317
|
+
}
|
|
318
|
+
async function sendToGA(eventType, props, options) {
|
|
319
|
+
const gaEvent = GaEventType[eventType] || toSnakeCase(eventType);
|
|
320
|
+
const {
|
|
321
|
+
items,
|
|
322
|
+
currency,
|
|
323
|
+
value,
|
|
324
|
+
transaction_id,
|
|
325
|
+
tax,
|
|
326
|
+
shipping,
|
|
327
|
+
coupon,
|
|
328
|
+
...rest
|
|
329
|
+
} = props;
|
|
330
|
+
const params = {
|
|
331
|
+
session_id: options.numericSessionId,
|
|
332
|
+
engagement_time_msec: 1,
|
|
333
|
+
session_engaged: "1"
|
|
334
|
+
};
|
|
335
|
+
if (currency) params.currency = currency;
|
|
336
|
+
if (value !== void 0) params.value = value;
|
|
337
|
+
if (transaction_id) params.transaction_id = transaction_id;
|
|
338
|
+
if (tax !== void 0) params.tax = tax;
|
|
339
|
+
if (shipping !== void 0) params.shipping = shipping;
|
|
340
|
+
if (coupon) params.coupon = coupon;
|
|
341
|
+
if (items) params.items = items;
|
|
342
|
+
const passthroughKeys = [
|
|
343
|
+
"item_list_id",
|
|
344
|
+
"data_provider",
|
|
345
|
+
"event_source",
|
|
346
|
+
"id",
|
|
347
|
+
"search_term",
|
|
348
|
+
"total",
|
|
349
|
+
"total_text",
|
|
350
|
+
"total_vector",
|
|
351
|
+
"page_location",
|
|
352
|
+
"page_title",
|
|
353
|
+
"page_referrer",
|
|
354
|
+
"language",
|
|
355
|
+
"test_marker",
|
|
356
|
+
"yp_platform",
|
|
357
|
+
"yp_platform_version",
|
|
358
|
+
"yp_user_agent"
|
|
359
|
+
];
|
|
360
|
+
for (const key of passthroughKeys) {
|
|
361
|
+
if (rest[key] !== void 0) params[key] = rest[key];
|
|
362
|
+
}
|
|
363
|
+
const body = {
|
|
364
|
+
client_id: options.clientId || options.sessionId,
|
|
365
|
+
timestamp_micros: Date.now() * 1e3,
|
|
366
|
+
events: [{ name: gaEvent, params }]
|
|
367
|
+
};
|
|
368
|
+
if (options.userId) body.user_id = options.userId;
|
|
369
|
+
const sep = options.gaProxyUrl.includes("?") ? "&" : "?";
|
|
370
|
+
const url = options.debug ? `${options.gaProxyUrl}${sep}debug=1` : options.gaProxyUrl;
|
|
371
|
+
try {
|
|
372
|
+
const res = await fetch(url, {
|
|
373
|
+
method: "POST",
|
|
374
|
+
headers: { "Content-Type": "application/json" },
|
|
375
|
+
body: JSON.stringify(body)
|
|
376
|
+
});
|
|
377
|
+
if (options.debug) {
|
|
378
|
+
console.log(`[YesPlz Analytics] GA MP \u2192 ${gaEvent} (status ${res.status})`);
|
|
379
|
+
}
|
|
380
|
+
} catch (e) {
|
|
381
|
+
if (options.debug) {
|
|
382
|
+
console.log(`[YesPlz Analytics] GA MP error: ${e.message}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// src/beacon.ts
|
|
388
|
+
var Beacon = class {
|
|
389
|
+
constructor(options) {
|
|
390
|
+
this.options = options;
|
|
391
|
+
this.queue = [];
|
|
392
|
+
this.isSending = false;
|
|
393
|
+
this.debounceTimer = null;
|
|
394
|
+
}
|
|
395
|
+
enqueue(event) {
|
|
396
|
+
this.queue.push(event);
|
|
397
|
+
this.scheduleSend();
|
|
398
|
+
}
|
|
399
|
+
enqueueAll(events) {
|
|
400
|
+
this.queue.push(...events);
|
|
401
|
+
this.scheduleSend();
|
|
402
|
+
}
|
|
403
|
+
scheduleSend() {
|
|
404
|
+
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
405
|
+
this.debounceTimer = setTimeout(() => {
|
|
406
|
+
void this.send();
|
|
407
|
+
}, this.options.debounceMs ?? 1e3);
|
|
408
|
+
}
|
|
409
|
+
async send(retryCount = 0) {
|
|
410
|
+
if (this.queue.length === 0) return;
|
|
411
|
+
if (!this.options.apiEndpoint) return;
|
|
412
|
+
if (this.isSending) return;
|
|
413
|
+
this.isSending = true;
|
|
414
|
+
const eventsToSend = this.queue.splice(0, this.queue.length);
|
|
415
|
+
const events = eventsToSend.map((event) => {
|
|
416
|
+
const merged = {
|
|
417
|
+
...event,
|
|
418
|
+
...this.options.defaultParams ?? {}
|
|
419
|
+
};
|
|
420
|
+
const rawId = event.extra_data.id;
|
|
421
|
+
if (rawId && this.options.idPrefix) {
|
|
422
|
+
merged.extra_data = {
|
|
423
|
+
...event.extra_data,
|
|
424
|
+
id: `${this.options.idPrefix}${rawId}`
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
return merged;
|
|
428
|
+
});
|
|
429
|
+
try {
|
|
430
|
+
const headers = { "Content-Type": "application/json" };
|
|
431
|
+
if (this.options.apiToken) headers.Authorization = `Token ${this.options.apiToken}`;
|
|
432
|
+
const res = await fetch(this.options.apiEndpoint, {
|
|
433
|
+
method: "POST",
|
|
434
|
+
headers,
|
|
435
|
+
body: JSON.stringify(events)
|
|
436
|
+
});
|
|
437
|
+
if (res.status > 299) {
|
|
438
|
+
throw new Error(`Failed to send events (status ${res.status})`);
|
|
439
|
+
}
|
|
440
|
+
if (this.options.debug) {
|
|
441
|
+
console.log(`[YesPlz Analytics] Sync: ${events.length} events sent`);
|
|
442
|
+
}
|
|
443
|
+
await Storage.removeItem(StorageKeys.PENDING_EVENTS);
|
|
444
|
+
} catch (e) {
|
|
445
|
+
if (this.options.debug) {
|
|
446
|
+
console.log(`[YesPlz Analytics] Sync failed: ${e.message}`);
|
|
447
|
+
}
|
|
448
|
+
this.queue.unshift(...eventsToSend);
|
|
449
|
+
this.isSending = false;
|
|
450
|
+
if (retryCount < 1) {
|
|
451
|
+
setTimeout(() => void this.send(retryCount + 1), 2e3);
|
|
452
|
+
}
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
this.isSending = false;
|
|
456
|
+
if (this.queue.length > 0) this.scheduleSend();
|
|
457
|
+
}
|
|
458
|
+
async flush() {
|
|
459
|
+
if (this.debounceTimer) {
|
|
460
|
+
clearTimeout(this.debounceTimer);
|
|
461
|
+
this.debounceTimer = null;
|
|
462
|
+
}
|
|
463
|
+
await this.send();
|
|
464
|
+
}
|
|
465
|
+
async savePending() {
|
|
466
|
+
if (this.queue.length === 0) return;
|
|
467
|
+
const toSave = this.queue.slice(-100);
|
|
468
|
+
await Storage.setJSON(StorageKeys.PENDING_EVENTS, toSave);
|
|
469
|
+
this.queue = [];
|
|
470
|
+
}
|
|
471
|
+
async restorePending() {
|
|
472
|
+
const pending = await Storage.getJSON(StorageKeys.PENDING_EVENTS, []);
|
|
473
|
+
if (pending.length > 0) {
|
|
474
|
+
this.queue.unshift(...pending);
|
|
475
|
+
await Storage.removeItem(StorageKeys.PENDING_EVENTS);
|
|
476
|
+
this.scheduleSend();
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
// src/analytics.ts
|
|
482
|
+
var DEFAULT_DEBOUNCE_MS = 1e3;
|
|
483
|
+
var DEFAULT_API_BASE_URL = "https://api.yesplz.ai";
|
|
484
|
+
var DEFAULT_GA_PROXY_BASE_URL = "https://ypsh-admin.yesplz.ai/api/mp/collect";
|
|
485
|
+
function buildUserAgent() {
|
|
486
|
+
const os = import_react_native.Platform.OS;
|
|
487
|
+
const version = import_react_native.Platform.Version;
|
|
488
|
+
return `YesPlzSDK-RN/${VERSION} (${os}; ${String(version)})`;
|
|
489
|
+
}
|
|
490
|
+
var Analytics = class {
|
|
491
|
+
constructor() {
|
|
492
|
+
this.config = {
|
|
493
|
+
debug: false,
|
|
494
|
+
debounceMs: DEFAULT_DEBOUNCE_MS,
|
|
495
|
+
cacheUserId: true,
|
|
496
|
+
idPrefix: ""
|
|
497
|
+
};
|
|
498
|
+
this.cid = null;
|
|
499
|
+
this.sessionId = null;
|
|
500
|
+
this.userId = null;
|
|
501
|
+
this.userProperties = {};
|
|
502
|
+
this.isInitialized = false;
|
|
503
|
+
this.initPromise = null;
|
|
504
|
+
this.beacon = null;
|
|
505
|
+
this.pendingTracks = [];
|
|
506
|
+
this.appStateSubscription = null;
|
|
507
|
+
}
|
|
508
|
+
init(appId, config = {}) {
|
|
509
|
+
if (this.isInitialized || this.initPromise) return this;
|
|
510
|
+
this.initPromise = this.initInternal(appId, config);
|
|
511
|
+
void this.initPromise;
|
|
512
|
+
return this;
|
|
513
|
+
}
|
|
514
|
+
ready() {
|
|
515
|
+
return this.initPromise ?? Promise.resolve();
|
|
516
|
+
}
|
|
517
|
+
async initInternal(appId, config) {
|
|
518
|
+
const normalizedAppId = String(appId).trim();
|
|
519
|
+
if (!normalizedAppId) {
|
|
520
|
+
throw new Error("[YesPlz Analytics] appId must be a non-empty string");
|
|
521
|
+
}
|
|
522
|
+
const apiBaseUrl = (config.apiBaseUrl || DEFAULT_API_BASE_URL).replace(/\/$/, "");
|
|
523
|
+
const apiEndpoint = config.apiEndpoint || `${apiBaseUrl}/api/v1/retailer/${encodeURIComponent(normalizedAppId)}/interactions/`;
|
|
524
|
+
const gaProxyBaseUrl = config.gaProxyBaseUrl || DEFAULT_GA_PROXY_BASE_URL;
|
|
525
|
+
const gaProxySep = gaProxyBaseUrl.includes("?") ? "&" : "?";
|
|
526
|
+
const gaProxyUrl = `${gaProxyBaseUrl}${gaProxySep}shop=${encodeURIComponent(normalizedAppId)}`;
|
|
527
|
+
const mergedDefaultParams = {
|
|
528
|
+
...config.defaultParams || {}
|
|
529
|
+
};
|
|
530
|
+
if (config.testMarker) {
|
|
531
|
+
mergedDefaultParams.test_marker = config.testMarker;
|
|
532
|
+
}
|
|
533
|
+
this.config = {
|
|
534
|
+
...this.config,
|
|
535
|
+
appId: normalizedAppId,
|
|
536
|
+
apiEndpoint,
|
|
537
|
+
apiBaseUrl,
|
|
538
|
+
apiToken: config.apiToken ?? null,
|
|
539
|
+
gaProxyBaseUrl,
|
|
540
|
+
gaProxyUrl,
|
|
541
|
+
idPrefix: config.idPrefix || "",
|
|
542
|
+
defaultParams: mergedDefaultParams,
|
|
543
|
+
testMarker: config.testMarker,
|
|
544
|
+
debug: config.debug ?? false,
|
|
545
|
+
cacheUserId: config.cacheUserId ?? true,
|
|
546
|
+
debounceMs: config.debounceMs || DEFAULT_DEBOUNCE_MS
|
|
547
|
+
};
|
|
548
|
+
const identity = await loadIdentity();
|
|
549
|
+
this.cid = identity.cid;
|
|
550
|
+
this.sessionId = identity.sessionId;
|
|
551
|
+
this.userId = identity.userId;
|
|
552
|
+
this.userProperties = identity.userProperties;
|
|
553
|
+
if (config.userId) {
|
|
554
|
+
await this.setUserInternal(config.userId, config.userProperties || {});
|
|
555
|
+
}
|
|
556
|
+
if (apiEndpoint) {
|
|
557
|
+
this.beacon = new Beacon({
|
|
558
|
+
apiEndpoint,
|
|
559
|
+
apiToken: this.config.apiToken,
|
|
560
|
+
defaultParams: this.config.defaultParams,
|
|
561
|
+
idPrefix: this.config.idPrefix,
|
|
562
|
+
debounceMs: this.config.debounceMs,
|
|
563
|
+
debug: this.config.debug
|
|
564
|
+
});
|
|
565
|
+
await this.beacon.restorePending();
|
|
566
|
+
}
|
|
567
|
+
this.appStateSubscription = import_react_native.AppState.addEventListener(
|
|
568
|
+
"change",
|
|
569
|
+
(state) => {
|
|
570
|
+
if (state === "background" || state === "inactive") {
|
|
571
|
+
void this.beacon?.savePending();
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
);
|
|
575
|
+
this.isInitialized = true;
|
|
576
|
+
this.log("Initialized");
|
|
577
|
+
const queued = this.pendingTracks.splice(0, this.pendingTracks.length);
|
|
578
|
+
for (const item of queued) {
|
|
579
|
+
void this.dispatchTrack(item.eventType, item.properties);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
async setUser(userId, userProperties = {}) {
|
|
583
|
+
await this.ready();
|
|
584
|
+
if (userId === null) {
|
|
585
|
+
this.userId = null;
|
|
586
|
+
this.userProperties = {};
|
|
587
|
+
await clearUser();
|
|
588
|
+
this.log("User cleared");
|
|
589
|
+
return this;
|
|
590
|
+
}
|
|
591
|
+
await this.setUserInternal(userId, userProperties);
|
|
592
|
+
return this;
|
|
593
|
+
}
|
|
594
|
+
async setUserInternal(userId, userProperties) {
|
|
595
|
+
const id = String(userId);
|
|
596
|
+
if (id.length === 0) {
|
|
597
|
+
console.error("[YesPlz Analytics] User ID must be a non-empty value");
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
this.userId = id;
|
|
601
|
+
this.userProperties = JSON.parse(JSON.stringify(userProperties));
|
|
602
|
+
await saveUser(this.userId, this.userProperties, this.config.cacheUserId);
|
|
603
|
+
this.log(`User set: ${id}`);
|
|
604
|
+
}
|
|
605
|
+
getUser() {
|
|
606
|
+
if (!this.userId) return null;
|
|
607
|
+
return { userId: this.userId, userProperties: this.userProperties };
|
|
608
|
+
}
|
|
609
|
+
getSessionId() {
|
|
610
|
+
return this.sessionId;
|
|
611
|
+
}
|
|
612
|
+
getClientId() {
|
|
613
|
+
return this.cid;
|
|
614
|
+
}
|
|
615
|
+
track(eventType, properties = {}) {
|
|
616
|
+
if (typeof eventType !== "string" || eventType.length === 0) {
|
|
617
|
+
console.error("[YesPlz Analytics] Event type must be a non-empty string");
|
|
618
|
+
return this;
|
|
619
|
+
}
|
|
620
|
+
if (!this.isInitialized) {
|
|
621
|
+
this.pendingTracks.push({
|
|
622
|
+
eventType,
|
|
623
|
+
properties: JSON.parse(JSON.stringify(properties))
|
|
624
|
+
});
|
|
625
|
+
return this;
|
|
626
|
+
}
|
|
627
|
+
void this.dispatchTrack(eventType, properties);
|
|
628
|
+
return this;
|
|
629
|
+
}
|
|
630
|
+
async dispatchTrack(eventType, properties) {
|
|
631
|
+
const props = JSON.parse(JSON.stringify(properties));
|
|
632
|
+
await handleAttribution(eventType, props, this.config.idPrefix);
|
|
633
|
+
if (eventType === EventType.Purchase && !props.transaction_id) {
|
|
634
|
+
props.transaction_id = generateId();
|
|
635
|
+
}
|
|
636
|
+
if (props.id && !Array.isArray(props.items)) {
|
|
637
|
+
props.items = [{ item_id: props.id, ...props }];
|
|
638
|
+
}
|
|
639
|
+
const metadata = this.collectMetadata();
|
|
640
|
+
Object.assign(props, metadata);
|
|
641
|
+
props.data_provider = "yesplz";
|
|
642
|
+
if (this.config.testMarker) {
|
|
643
|
+
props.test_marker = this.config.testMarker;
|
|
644
|
+
}
|
|
645
|
+
await this.sendGA(eventType, props);
|
|
646
|
+
const apiEventType = GaEventTypeReverse[eventType] || eventType;
|
|
647
|
+
if (API_EVENTS.includes(apiEventType)) {
|
|
648
|
+
this.queueApi(apiEventType, props, metadata);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
async sendGA(eventType, props) {
|
|
652
|
+
if (!this.config.gaProxyUrl) return;
|
|
653
|
+
if (!this.cid || !this.sessionId) return;
|
|
654
|
+
const gaEventName = GaEventType[eventType] ?? eventType;
|
|
655
|
+
await sendToGA(eventType, props, {
|
|
656
|
+
gaProxyUrl: this.config.gaProxyUrl,
|
|
657
|
+
clientId: this.cid,
|
|
658
|
+
sessionId: this.sessionId,
|
|
659
|
+
numericSessionId: numericSessionId(this.sessionId),
|
|
660
|
+
userId: this.userId,
|
|
661
|
+
debug: this.config.debug
|
|
662
|
+
});
|
|
663
|
+
this.log(`Track: ${gaEventName}`, props.items?.length ? { items: props.items.length } : void 0);
|
|
664
|
+
}
|
|
665
|
+
queueApi(eventType, props, metadata) {
|
|
666
|
+
if (!this.beacon) return;
|
|
667
|
+
const items = props.items && props.items.length > 0 ? props.items : [props];
|
|
668
|
+
const fallbackUrl = `app://${this.config.appId ?? "yesplz"}/`;
|
|
669
|
+
const url = typeof props.page_location === "string" && props.page_location.length > 0 ? props.page_location : fallbackUrl;
|
|
670
|
+
const referrerUrl = typeof props.page_referrer === "string" && props.page_referrer.length > 0 ? props.page_referrer : null;
|
|
671
|
+
const events = items.map((item) => {
|
|
672
|
+
const merged = {
|
|
673
|
+
...props,
|
|
674
|
+
...item,
|
|
675
|
+
...metadata
|
|
676
|
+
};
|
|
677
|
+
const itemId = item.item_id;
|
|
678
|
+
if (itemId) merged.id = itemId;
|
|
679
|
+
merged.identifier_type = eventType.startsWith("yp_brand_") ? "brand_name" : "product_id";
|
|
680
|
+
const event = {
|
|
681
|
+
event_type: eventType,
|
|
682
|
+
url,
|
|
683
|
+
referrer_url: referrerUrl,
|
|
684
|
+
user_id: this.userId,
|
|
685
|
+
session_id: this.sessionId,
|
|
686
|
+
extra_data: merged
|
|
687
|
+
};
|
|
688
|
+
if (Object.keys(this.userProperties).length > 0) {
|
|
689
|
+
event.extra_user_data = this.userProperties;
|
|
690
|
+
}
|
|
691
|
+
return event;
|
|
692
|
+
});
|
|
693
|
+
this.beacon.enqueueAll(events);
|
|
694
|
+
}
|
|
695
|
+
collectMetadata() {
|
|
696
|
+
const metadata = {
|
|
697
|
+
yp_client_time: Date.now(),
|
|
698
|
+
yp_platform: import_react_native.Platform.OS,
|
|
699
|
+
yp_platform_version: String(import_react_native.Platform.Version),
|
|
700
|
+
yp_user_agent: buildUserAgent(),
|
|
701
|
+
yp_timezone_offset: (/* @__PURE__ */ new Date()).getTimezoneOffset()
|
|
702
|
+
};
|
|
703
|
+
try {
|
|
704
|
+
metadata.yp_timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
705
|
+
} catch {
|
|
706
|
+
}
|
|
707
|
+
if (this.cid) metadata.google_client_id = this.cid;
|
|
708
|
+
return metadata;
|
|
709
|
+
}
|
|
710
|
+
async flush() {
|
|
711
|
+
await this.ready();
|
|
712
|
+
await this.beacon?.flush();
|
|
713
|
+
return this;
|
|
714
|
+
}
|
|
715
|
+
async reset() {
|
|
716
|
+
await this.ready();
|
|
717
|
+
this.userId = null;
|
|
718
|
+
this.userProperties = {};
|
|
719
|
+
await clearUser();
|
|
720
|
+
await Storage.removeItem(StorageKeys.PENDING_EVENTS);
|
|
721
|
+
await Storage.removeItem(StorageKeys.ATTRIBUTION);
|
|
722
|
+
const newSession = generateId();
|
|
723
|
+
this.sessionId = newSession;
|
|
724
|
+
await saveSession(newSession);
|
|
725
|
+
this.log("Reset");
|
|
726
|
+
return this;
|
|
727
|
+
}
|
|
728
|
+
teardown() {
|
|
729
|
+
this.appStateSubscription?.remove();
|
|
730
|
+
this.appStateSubscription = null;
|
|
731
|
+
}
|
|
732
|
+
log(message, data) {
|
|
733
|
+
if (this.config.debug) {
|
|
734
|
+
console.log(`[YesPlz Analytics] ${message}`, data ?? "");
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
var globalRef = globalThis;
|
|
739
|
+
var analytics = globalRef.__yesplz_analytics_rn__ || (globalRef.__yesplz_analytics_rn__ = new Analytics());
|
|
740
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
741
|
+
0 && (module.exports = {
|
|
742
|
+
Analytics,
|
|
743
|
+
EventType,
|
|
744
|
+
ItemListId,
|
|
745
|
+
VERSION,
|
|
746
|
+
analytics,
|
|
747
|
+
normalizeItemListId
|
|
748
|
+
});
|
|
749
|
+
//# sourceMappingURL=index.cjs.map
|