@salla.sa/analytics 2.14.548
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/LICENSE.md +674 -0
- package/README.md +46 -0
- package/dist/cjs/checkout.js +2 -0
- package/dist/cjs/checkout.js.map +1 -0
- package/dist/cjs/chunks/uuid-DMRtGC8c.js +2 -0
- package/dist/cjs/chunks/uuid-DMRtGC8c.js.map +1 -0
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/storefront.js +2 -0
- package/dist/cjs/storefront.js.map +1 -0
- package/dist/esm/checkout.js +2 -0
- package/dist/esm/checkout.js.map +1 -0
- package/dist/esm/chunks/uuid-BSpqiCZm.js +2 -0
- package/dist/esm/chunks/uuid-BSpqiCZm.js.map +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/storefront.js +2 -0
- package/dist/esm/storefront.js.map +1 -0
- package/package.json +67 -0
- package/src/checkout/address-added.js +46 -0
- package/src/checkout/address-updated.js +46 -0
- package/src/checkout/checkout-started.js +34 -0
- package/src/checkout/checkout-step-completed.js +18 -0
- package/src/checkout/checkout-step-viewed.js +17 -0
- package/src/checkout/coupon-applied.js +22 -0
- package/src/checkout/coupon-denied.js +20 -0
- package/src/checkout/coupon-entered.js +21 -0
- package/src/checkout/coupon-removed.js +21 -0
- package/src/checkout/event-transformer.js +53 -0
- package/src/checkout/index.js +197 -0
- package/src/checkout/initial-data.js +23 -0
- package/src/checkout/map-clicked.js +27 -0
- package/src/checkout/payment-failed.js +28 -0
- package/src/checkout/payment-info-entered.js +17 -0
- package/src/checkout/payment-pending.js +21 -0
- package/src/checkout/payment-submitted.js +22 -0
- package/src/checkout/payment-succeeded.js +23 -0
- package/src/checkout/sanitizers.js +21 -0
- package/src/checkout/session.js +80 -0
- package/src/index.js +7 -0
- package/src/shared/cookie-capture.js +17 -0
- package/src/shared/extra-context.js +23 -0
- package/src/shared/host-resolver.js +58 -0
- package/src/shared/tracker-registry.js +55 -0
- package/src/shared/uuid.js +10 -0
- package/src/storefront/basket-gap-product-added.js +29 -0
- package/src/storefront/cart-updated.js +41 -0
- package/src/storefront/cart-viewed.js +30 -0
- package/src/storefront/coupon-applied.js +26 -0
- package/src/storefront/coupon-denied.js +35 -0
- package/src/storefront/coupon-removed.js +27 -0
- package/src/storefront/event-transformer.js +90 -0
- package/src/storefront/fbt-product-added.js +29 -0
- package/src/storefront/index.js +524 -0
- package/src/storefront/market-selected.js +29 -0
- package/src/storefront/order-completed.js +12 -0
- package/src/storefront/product-added-to-wishlist.js +28 -0
- package/src/storefront/product-added.js +51 -0
- package/src/storefront/product-clicked.js +22 -0
- package/src/storefront/product-fields.js +15 -0
- package/src/storefront/product-list-filtered.js +56 -0
- package/src/storefront/product-list-viewed.js +80 -0
- package/src/storefront/product-removed-from-wishlist.js +29 -0
- package/src/storefront/product-removed.js +27 -0
- package/src/storefront/product-reordered.js +19 -0
- package/src/storefront/product-reviewed.js +28 -0
- package/src/storefront/product-shared.js +18 -0
- package/src/storefront/product-viewed.js +44 -0
- package/src/storefront/products-searched.js +14 -0
- package/src/storefront/promotion-clicked.js +18 -0
- package/src/storefront/promotion-viewed.js +18 -0
- package/src/storefront/recommendation-list-viewed.js +22 -0
- package/src/storefront/signed-in.js +29 -0
- package/src/storefront/signed-out.js +10 -0
- package/src/storefront/signed-up.js +21 -0
- package/src/storefront/user-profile-updated.js +44 -0
- package/src/storefront/wishlist-product-added-to-cart.js +32 -0
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
import { jitsuAnalytics, windowRuntime } from '@jitsu/js';
|
|
2
|
+
import EventTransformer from './event-transformer.js';
|
|
3
|
+
import { resolveAnalyticsHost } from '../shared/host-resolver.js';
|
|
4
|
+
import { captureSallaCookies } from '../shared/cookie-capture.js';
|
|
5
|
+
import { isValidTracker, replayHistory, pushEventToTrackers } from '../shared/tracker-registry.js';
|
|
6
|
+
import { applyExtraContext } from '../shared/extra-context.js';
|
|
7
|
+
import { generateSessionId } from '../shared/uuid.js';
|
|
8
|
+
|
|
9
|
+
const SESSION_STORAGE_KEY = 's-session-id';
|
|
10
|
+
const SESSION_TTL_MINUTES = 30;
|
|
11
|
+
|
|
12
|
+
export default class Analytics {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.history = [];
|
|
15
|
+
this.customTrackers = [];
|
|
16
|
+
this.context = {};
|
|
17
|
+
this._anonymousId = null;
|
|
18
|
+
this._sessionId = null;
|
|
19
|
+
this._jitsuConfig = null;
|
|
20
|
+
this.mappedEvents = {
|
|
21
|
+
'wishlist::added': 'Product Added to Wishlist',
|
|
22
|
+
'wishlist::removed': 'Product Removed from Wishlist',
|
|
23
|
+
'salla-products-list::products.fetched': ['Product List Viewed', 'Product List Filtered'],
|
|
24
|
+
'auth::registered': 'Signed Up',
|
|
25
|
+
'auth::logged.in': 'Signed In',
|
|
26
|
+
'auth::logged.out': 'Signed Out',
|
|
27
|
+
'share-element': 'Product Shared',
|
|
28
|
+
'promotion::viewed': 'Promotion Viewed',
|
|
29
|
+
'promotion::clicked': 'Promotion Clicked',
|
|
30
|
+
'salla::offer.promotion.viewed': 'Promotion Viewed',
|
|
31
|
+
'salla::offer.promotion.clicked': 'Promotion Clicked',
|
|
32
|
+
'cart::coupon.added': 'Coupon Applied',
|
|
33
|
+
'cart::coupon.deleted': 'Coupon Removed',
|
|
34
|
+
'cart::coupon.addition.failed': 'Coupon Denied',
|
|
35
|
+
'profile::updated' : 'User Profile Updated',
|
|
36
|
+
'cart::updated' : 'Cart Updated',
|
|
37
|
+
'scope::changed' : 'Market Selected',
|
|
38
|
+
'order::order.created': 'Product Reordered',
|
|
39
|
+
'salla-bought-together::list.viewed': 'Recommendation List Viewed',
|
|
40
|
+
'salla-bought-together::product.clicked': 'Product Clicked',
|
|
41
|
+
'salla-bought-together::product.added': 'Product Added',
|
|
42
|
+
'salla-basket-gap::product.added': 'Product Added',
|
|
43
|
+
};
|
|
44
|
+
Salla.event.onAny((event, details) => {
|
|
45
|
+
try {
|
|
46
|
+
if (Object.keys(this.mappedEvents).includes(event)) {
|
|
47
|
+
const mapped = this.mappedEvents[event];
|
|
48
|
+
const targets = Array.isArray(mapped) ? mapped : [mapped];
|
|
49
|
+
targets.forEach((mappedEventKey) => Salla.event.emit(mappedEventKey, details));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!Salla.config.get('jitsu.events', []).includes(event)) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const segmentEvent = EventTransformer.transform(event, details);
|
|
58
|
+
if (segmentEvent) {
|
|
59
|
+
this.track(segmentEvent.event, segmentEvent.properties);
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
Salla.log('Error in analytics event handling:', error);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
Salla.event.on('salla-cookies-bar::accepted', () => this.setConsent('accepted'));
|
|
67
|
+
Salla.event.on('salla-cookies-bar::rejected', () => this.setConsent('rejected'));
|
|
68
|
+
Salla.event.on('auth::logged.out', () => this.clearSessionId());
|
|
69
|
+
|
|
70
|
+
Salla.event.on('twilight::before.ready', (config) => {
|
|
71
|
+
this.debugAnalytics = jitsuAnalytics({
|
|
72
|
+
host: 'https://st.salla.dev',
|
|
73
|
+
writeKey: 'bbLD6Xqr1OKsDJfxZBCy7SJN1RIsrkBp:WEa99iY220v9qX7KiuAlML9ldSFJKW2p'
|
|
74
|
+
});
|
|
75
|
+
const { host, key: writeKey } = config?.jitsu || {};
|
|
76
|
+
this.jitsuAnalytics = null;
|
|
77
|
+
if (!host || !writeKey) {
|
|
78
|
+
salla.log('Jitsu configuration is missing. Analytics will not be tracked.');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
EventTransformer.registerEvents();
|
|
82
|
+
this._jitsuConfig = {
|
|
83
|
+
host: this.getAnalyticsHost(config),
|
|
84
|
+
writeKey,
|
|
85
|
+
cookieCapture: captureSallaCookies(),
|
|
86
|
+
runtime: this.getSafeJitsuRuntime(config),
|
|
87
|
+
...(Salla.helpers.isIframe() && { fetch: (url, options) => this.customFetch(url, options) }),
|
|
88
|
+
};
|
|
89
|
+
const consentPolicies = this.getConsentPolicies();
|
|
90
|
+
this.jitsuAnalytics = jitsuAnalytics({
|
|
91
|
+
...this._jitsuConfig,
|
|
92
|
+
...consentPolicies,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
config.jitsu.anonymous_id && this.jitsuAnalytics.setAnonymousId(config.jitsu.anonymous_id);
|
|
96
|
+
// Initialize and cache anonymous ID immediately after Jitsu is created
|
|
97
|
+
this._anonymousId = this.jitsuAnalytics?.user()?.anonymousId || null;
|
|
98
|
+
|
|
99
|
+
config.jitsu.session_id && this.setContext('session_id', config.jitsu.session_id);
|
|
100
|
+
// Initialize and cache anonymous ID immediately after Jitsu is created
|
|
101
|
+
this._sessionId = this.jitsuAnalytics?.getContextProperty('session_id');
|
|
102
|
+
|
|
103
|
+
this.setContext('store_id', config.store?.id);
|
|
104
|
+
this.setContext('version', config.jitsu?.version);
|
|
105
|
+
this.setContext('library',{twilight:Salla.versions});
|
|
106
|
+
this.setContext('theme',{
|
|
107
|
+
name: String(Salla.config.get('theme.name')),
|
|
108
|
+
version: String(Salla.config.get('headers.s-version-id'))
|
|
109
|
+
});
|
|
110
|
+
if(config.jitsu?.context?.extra){
|
|
111
|
+
this.setExtraContext(config.jitsu.context.extra);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (config.store?.scope?.id) {
|
|
115
|
+
// this.jitsuAnalytics.setContextProperty('scope', config.store?.scope);
|
|
116
|
+
this.setContext('scope', config.store?.scope);
|
|
117
|
+
}
|
|
118
|
+
if (config?.user?.type === 'user') {
|
|
119
|
+
this.setContext('traits', this.getFormattedUser(config.user));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Register default Salla tracker
|
|
123
|
+
this.registerTracker({
|
|
124
|
+
name: 'DefaultSallaTracker',
|
|
125
|
+
track: (eventName, payload) => this.jitsuAnalytics.track(eventName, payload),
|
|
126
|
+
page: (payload) => this.jitsuAnalytics.page(payload),
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
Salla.onReady(() => {
|
|
131
|
+
this.initSessionId();
|
|
132
|
+
const pagePayload = { name: Salla.config.get('page.slug', ''), id:Salla.config.get('page.id', '')?.toString()};
|
|
133
|
+
this.setContext('page', pagePayload);
|
|
134
|
+
this.attachViewedSource();
|
|
135
|
+
Salla.config.set('jitsu.anonymous_id', this._anonymousId);
|
|
136
|
+
Salla.config.set('jitsu.session_id', this._sessionId);
|
|
137
|
+
this.page(pagePayload);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
attachViewedSource() {
|
|
142
|
+
try {
|
|
143
|
+
const url = new URL(window.location.href);
|
|
144
|
+
const value = url.searchParams.get('_svs');
|
|
145
|
+
if (!value) return;
|
|
146
|
+
url.searchParams.delete('_svs');
|
|
147
|
+
window.history.replaceState(window.history.state, '', url.toString());
|
|
148
|
+
const sanitized = value.replace(/[^\p{L}\p{N}\-_ ]/gu, '').trim().slice(0, 100);
|
|
149
|
+
if (!sanitized) return;
|
|
150
|
+
this.setContext('viewed_source', sanitized);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
Salla.log('Error reading viewed_source from URL:', error);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
setExtraContext(extra){
|
|
157
|
+
applyExtraContext((key, value) => this.setContext(key, value), extra, (...args) => Salla.log(...args));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
getAnalyticsHost(config) {
|
|
161
|
+
return resolveAnalyticsHost(config?.store?.url, {
|
|
162
|
+
iframe: Salla.helpers.isIframe(),
|
|
163
|
+
iframeHost: config?.jitsu?.host,
|
|
164
|
+
log: (...args) => Salla.log(...args),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
setContext(key, value){
|
|
169
|
+
this.jitsuAnalytics?.setContextProperty(key, value);
|
|
170
|
+
this.debugAnalytics?.setContextProperty(key, value);
|
|
171
|
+
this.context[key] = value;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Set app-specific context for partner applications
|
|
176
|
+
* @param {string} appId - The unique identifier for the partner app (e.g., 'webengage')
|
|
177
|
+
* @param {object} payload - The context data to store for this app
|
|
178
|
+
*/
|
|
179
|
+
setAppContext(appId, payload) {
|
|
180
|
+
if (!this.jitsuAnalytics) {
|
|
181
|
+
Salla.log('setAppContext: Analytics not initialized yet');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!appId || typeof appId !== 'string') {
|
|
186
|
+
Salla.log('setAppContext: appId must be a non-empty string');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!payload || typeof payload !== 'object') {
|
|
191
|
+
Salla.log('setAppContext: payload must be an object');
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const currentAppsContext = this.context.apps_context || {};
|
|
196
|
+
currentAppsContext[appId] = {
|
|
197
|
+
...currentAppsContext[appId],
|
|
198
|
+
...payload
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
this.setContext('apps_context', currentAppsContext);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Execute a callback with scoped app context. All track() calls within
|
|
206
|
+
* the callback will include the scoped context in payload.context.
|
|
207
|
+
* Supports both sync and async callbacks; for async callbacks,
|
|
208
|
+
* apps_context stays populated until the returned promise settles.
|
|
209
|
+
* @param {string} appId - The app identifier
|
|
210
|
+
* @param {function} callback - Receives a scope object with setContext(key, value)
|
|
211
|
+
* @returns {*} The callback's return value (a Promise if the callback is async)
|
|
212
|
+
*/
|
|
213
|
+
withAppScope(appId, callback) {
|
|
214
|
+
if (!appId || typeof appId !== 'string') {
|
|
215
|
+
Salla.log('withAppScope: appId must be a non-empty string');
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (typeof callback !== 'function') {
|
|
219
|
+
Salla.log('withAppScope: callback must be a function');
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Snapshot only this scope's entry against the live apps_context,
|
|
224
|
+
// so overlapping scopes for *other* appIds aren't clobbered when we
|
|
225
|
+
// restore. The live apps_context is re-read inside restore().
|
|
226
|
+
const hadAppEntry = this.context.apps_context != null
|
|
227
|
+
&& Object.prototype.hasOwnProperty.call(this.context.apps_context, appId);
|
|
228
|
+
const previousAppEntry = hadAppEntry ? this.context.apps_context[appId] : undefined;
|
|
229
|
+
|
|
230
|
+
const self = this;
|
|
231
|
+
const scope = {
|
|
232
|
+
appId,
|
|
233
|
+
setContext(key, value) {
|
|
234
|
+
self.setAppContext(appId, { [key]: value });
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// Seed the entry so the appId is present in apps_context even
|
|
239
|
+
// if the callback never calls scope.setContext.
|
|
240
|
+
self.setAppContext(appId, {});
|
|
241
|
+
|
|
242
|
+
// Cleanup via macrotask so Jitsu's async plugin processing
|
|
243
|
+
// (microtask/Promise-based) reads the scoped context before we clear it
|
|
244
|
+
const restore = () => setTimeout(() => {
|
|
245
|
+
const current = self.context.apps_context
|
|
246
|
+
? { ...self.context.apps_context }
|
|
247
|
+
: {};
|
|
248
|
+
if (hadAppEntry) {
|
|
249
|
+
current[appId] = previousAppEntry;
|
|
250
|
+
} else {
|
|
251
|
+
delete current[appId];
|
|
252
|
+
}
|
|
253
|
+
self.setContext(
|
|
254
|
+
'apps_context',
|
|
255
|
+
Object.keys(current).length ? current : undefined,
|
|
256
|
+
);
|
|
257
|
+
}, 0);
|
|
258
|
+
|
|
259
|
+
let result;
|
|
260
|
+
try {
|
|
261
|
+
result = callback(scope);
|
|
262
|
+
} catch (err) {
|
|
263
|
+
restore();
|
|
264
|
+
throw err;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (result && typeof result.then === 'function') {
|
|
268
|
+
// Promise.resolve normalizes any thenable (including ones without
|
|
269
|
+
// .finally) into a real Promise so cleanup is guaranteed.
|
|
270
|
+
return Promise.resolve(result).finally(restore);
|
|
271
|
+
}
|
|
272
|
+
restore();
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Use normal fetch with keepalive instead of beacon, as beacon is mostly blocked by ad blockers.
|
|
277
|
+
// keepalive ensures the request completes even if the page reloads or navigates away.
|
|
278
|
+
customFetch(url, options) {
|
|
279
|
+
return fetch(url, { ...options, keepalive: true });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
getSafeJitsuRuntime(config = {}) {
|
|
283
|
+
if (typeof window === 'undefined') {
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const runtime = windowRuntime(config);
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
...runtime,
|
|
291
|
+
pageUrl: () => this.normalizeUrlForDecodeURIComponent(runtime.pageUrl()),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
normalizeUrlForDecodeURIComponent(url) {
|
|
296
|
+
try {
|
|
297
|
+
const parsedUrl = new URL(url);
|
|
298
|
+
|
|
299
|
+
if (parsedUrl.search) {
|
|
300
|
+
parsedUrl.search = new URLSearchParams(parsedUrl.search).toString();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return parsedUrl.toString();
|
|
304
|
+
} catch (error) {
|
|
305
|
+
Salla.log('Error normalizing analytics page URL:', error);
|
|
306
|
+
return url;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
track(eventName, payload = {}) {
|
|
311
|
+
this.refreshSessionIdTTL();
|
|
312
|
+
payload = this.finalPayload(payload);
|
|
313
|
+
this.history.push({ action: 'track', eventName, payload });
|
|
314
|
+
this.pushEventToCustomTrackers('track', eventName, payload);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
log(eventName, payload = {}) {
|
|
318
|
+
return this.debugAnalytics?.track(eventName, payload).catch((error) => {
|
|
319
|
+
Salla.log("Error tracking event:", error);
|
|
320
|
+
return Promise.reject(error);
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
page(payload = {}) {
|
|
325
|
+
this.refreshSessionIdTTL();
|
|
326
|
+
payload = this.finalPayload(payload);
|
|
327
|
+
payload.id = (payload.id || Salla.config.get('page.id', ''))?.toString();
|
|
328
|
+
payload.name = payload.name || Salla.config.get('page.slug');
|
|
329
|
+
payload.keywords = payload.keywords || document.querySelector('meta[name=keywords]')?.content;
|
|
330
|
+
payload.referrer = payload.referrer || document.referrer;
|
|
331
|
+
|
|
332
|
+
this.history.push({ action: 'page', payload });
|
|
333
|
+
this.pushEventToCustomTrackers('page', payload);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
finalPayload(payload = {}) {
|
|
337
|
+
let defaultPayload = {};
|
|
338
|
+
this.attachRealtimeContext();
|
|
339
|
+
payload.event_id = payload.event_id || this.generateId();
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
...defaultPayload,
|
|
343
|
+
...payload
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
attachRealtimeContext() {
|
|
348
|
+
try {
|
|
349
|
+
const hasCookiesConsent = Salla.storage.get('cookies-consent') === 'accepted';
|
|
350
|
+
const sensitiveKeys = ['user.id', 'cart.user_id'];
|
|
351
|
+
const keys = ['user.id', 'cart.id', 'cart.user_id', 'cart.count', 'cart.summary.count', 'scope.id', 'branch-choosed-before']
|
|
352
|
+
.filter((key) => hasCookiesConsent || !sensitiveKeys.includes(key));
|
|
353
|
+
const storageData = keys.reduce((acc, key) => {
|
|
354
|
+
const value = Salla.storage.get(key);
|
|
355
|
+
if (value) {
|
|
356
|
+
acc[key.replace('.', '_')] = value;
|
|
357
|
+
}
|
|
358
|
+
return acc;
|
|
359
|
+
}, {});
|
|
360
|
+
|
|
361
|
+
if (!hasCookiesConsent) {
|
|
362
|
+
// Ensure sensitive IDs are removed even if an old context existed.
|
|
363
|
+
delete storageData.user_id;
|
|
364
|
+
delete storageData.cart_user_id;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
this.setContext('localStorage', Object.keys(storageData).length > 0 ? storageData : undefined);
|
|
368
|
+
|
|
369
|
+
//This keys should be added only inside web view to unify the experience between native and webview
|
|
370
|
+
['device', 'os', 'app'].forEach((key) => {
|
|
371
|
+
const value = Salla.storage.get(key);
|
|
372
|
+
if (value) {
|
|
373
|
+
this.setContext(key, value);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
} catch (error) {
|
|
377
|
+
Salla.analytics.log('attachRealtimeContext', {error: error.message});
|
|
378
|
+
Salla.log('Error attaching realtime context:', error);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const utms = Salla.storage.getWithTTL('s-utms');
|
|
382
|
+
if (!utms) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const {
|
|
386
|
+
's-utm-campaign': name,
|
|
387
|
+
's-utm-source': source,
|
|
388
|
+
's-utm-medium': medium,
|
|
389
|
+
's-utm-term': term,
|
|
390
|
+
's-utm-content': content,
|
|
391
|
+
's-utm-referrer': referrer
|
|
392
|
+
} = utms;
|
|
393
|
+
|
|
394
|
+
this.setContext('campaign', {
|
|
395
|
+
name,
|
|
396
|
+
source,
|
|
397
|
+
medium,
|
|
398
|
+
term,
|
|
399
|
+
content,
|
|
400
|
+
referrer
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
registerTracker(tracker) {
|
|
405
|
+
if (!isValidTracker(tracker, this.customTrackers, (...args) => Salla.log(...args))) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
this.customTrackers.push(tracker);
|
|
409
|
+
replayHistory(tracker, this.history);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
pushEventToCustomTrackers(action, eventName, payload = {}) {
|
|
413
|
+
return pushEventToTrackers(this.customTrackers, action, eventName, payload, (...args) => Salla.log(...args));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
getConsentState() {
|
|
417
|
+
return Salla.storage.get('cookies-consent', 'unknown');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
getConsentPolicies(state = null) {
|
|
421
|
+
if (!Salla.config.get('store.features')?.includes('website-cookies')) return {};
|
|
422
|
+
state = state || this.getConsentState();
|
|
423
|
+
const isAccepted = state === 'accepted';
|
|
424
|
+
return {
|
|
425
|
+
privacy: {
|
|
426
|
+
dontSend: false,
|
|
427
|
+
disableUserIds: !isAccepted,
|
|
428
|
+
ipPolicy: isAccepted ? 'keep' : 'remove',
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
setConsent(state) {
|
|
434
|
+
if (!Salla.config.get('store.features')?.includes('website-cookies')) return;
|
|
435
|
+
if (!['accepted', 'rejected'].includes(state)) return;
|
|
436
|
+
|
|
437
|
+
const previousState = this.getConsentState();
|
|
438
|
+
|
|
439
|
+
if (!this._jitsuConfig || !this.jitsuAnalytics) return;
|
|
440
|
+
if (previousState === state) return;
|
|
441
|
+
|
|
442
|
+
try {
|
|
443
|
+
const consentPolicies = this.getConsentPolicies(state);
|
|
444
|
+
this.jitsuAnalytics = jitsuAnalytics({
|
|
445
|
+
...this._jitsuConfig,
|
|
446
|
+
...consentPolicies,
|
|
447
|
+
});
|
|
448
|
+
this._anonymousId = this.jitsuAnalytics?.user()?.anonymousId || null;
|
|
449
|
+
Salla.config.set('jitsu.anonymous_id', this._anonymousId);
|
|
450
|
+
|
|
451
|
+
// Re-apply stored context, no need to skip traits because it will not sent due to disableUserIds option
|
|
452
|
+
Object.entries(this.context).forEach(([key, value]) => {
|
|
453
|
+
this.jitsuAnalytics.setContextProperty(key, value);
|
|
454
|
+
});
|
|
455
|
+
} catch (error) {
|
|
456
|
+
Salla.log('Error re-initializing Jitsu on consent change:', error);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
getFormattedUser(user) {
|
|
461
|
+
user.phone = user.mobile;
|
|
462
|
+
user.country = user.country_code;
|
|
463
|
+
user.language = user.language_code;
|
|
464
|
+
user.currency = user.currency_code;
|
|
465
|
+
return user;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
generateId() {
|
|
469
|
+
return generateSessionId();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
generateSessionId() {
|
|
473
|
+
return this.generateId();
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
initSessionId() {
|
|
477
|
+
this._sessionId = this._sessionId || Salla.storage.getWithTTL(SESSION_STORAGE_KEY) || this.generateSessionId();
|
|
478
|
+
Salla.storage.setWithTTL(SESSION_STORAGE_KEY, this._sessionId, SESSION_TTL_MINUTES);
|
|
479
|
+
this.setContext('session_id', this._sessionId);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Sliding 30m inactivity: refresh TTL on track/page; mint new id if storage expired
|
|
483
|
+
refreshSessionIdTTL() {
|
|
484
|
+
const stored = Salla.storage.getWithTTL(SESSION_STORAGE_KEY);
|
|
485
|
+
if (!stored) {
|
|
486
|
+
this._sessionId = null;
|
|
487
|
+
this.initSessionId();
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
this._sessionId = stored;
|
|
491
|
+
Salla.storage.setWithTTL(SESSION_STORAGE_KEY, this._sessionId, SESSION_TTL_MINUTES);
|
|
492
|
+
this.setContext('session_id', this._sessionId);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
clearSessionId() {
|
|
496
|
+
this._sessionId = null;
|
|
497
|
+
Salla.storage.setWithTTL(SESSION_STORAGE_KEY, null, 0);
|
|
498
|
+
this.setContext('session_id', null);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
getAnonymousId() {
|
|
502
|
+
try {
|
|
503
|
+
// Return cached anonymous ID if available
|
|
504
|
+
if (this._anonymousId) {
|
|
505
|
+
return this._anonymousId;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Try to get from Jitsu if initialized
|
|
509
|
+
if (this.jitsuAnalytics) {
|
|
510
|
+
const anonymousId = this.jitsuAnalytics.user()?.anonymousId;
|
|
511
|
+
if (anonymousId) {
|
|
512
|
+
// Cache it for future use
|
|
513
|
+
this._anonymousId = anonymousId;
|
|
514
|
+
return anonymousId;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return null;
|
|
519
|
+
} catch (error) {
|
|
520
|
+
Salla.log('Error getting anonymous ID from Jitsu:', error);
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export default class MarketSelected {
|
|
2
|
+
constructor(details) {
|
|
3
|
+
this.scope = details.data;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
toSegment() {
|
|
7
|
+
return {
|
|
8
|
+
event: 'Market Selected',
|
|
9
|
+
properties: {
|
|
10
|
+
...this.getScope(this.scope),
|
|
11
|
+
prev: this.getScope(Salla.config.get('store.scope')),
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getScope(scope) {
|
|
17
|
+
scope = scope || {};
|
|
18
|
+
return {
|
|
19
|
+
id: scope.id?.toString(),
|
|
20
|
+
country: this.getScopeAttribute(scope, 'countries'),
|
|
21
|
+
currency: this.getScopeAttribute(scope, 'currencies'),
|
|
22
|
+
locale: this.getScopeAttribute(scope, 'languages'),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
getScopeAttribute(scope, key) {
|
|
26
|
+
scope = scope[key];
|
|
27
|
+
return Array.isArray(scope) ? scope[0] : scope;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export default class ProductAddedToWishlist {
|
|
2
|
+
constructor(details) {
|
|
3
|
+
this.product = details.data.product;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
toSegment() {
|
|
7
|
+
return {
|
|
8
|
+
event: 'Product Added to Wishlist',
|
|
9
|
+
properties: {
|
|
10
|
+
wishlist_id : salla.config.get('store.id') + '_' + salla.config.get('user.id'),
|
|
11
|
+
wishlist_name : salla.config.get('store.name'),
|
|
12
|
+
product_id : this.product.id?.toString(),
|
|
13
|
+
name: this.product.name,
|
|
14
|
+
brand: this.product.brand,
|
|
15
|
+
variant: this.product.variant,
|
|
16
|
+
price: this.product.price,
|
|
17
|
+
quantity: Math.max(1, Number(this.product.quantity)),
|
|
18
|
+
sku: this.product.sku || null,
|
|
19
|
+
position: Number(this.product.position || 0),
|
|
20
|
+
url: this.product.url,
|
|
21
|
+
image_url: this.product?.image || null,
|
|
22
|
+
category: this.product.category,
|
|
23
|
+
currency: Salla.config.currency()?.code,
|
|
24
|
+
brand: this.product.brand,
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export default class ProductAdded {
|
|
2
|
+
constructor(details) {
|
|
3
|
+
this.item = details[0];
|
|
4
|
+
this.source = Salla.cart?.api?.addToCartSource || null;
|
|
5
|
+
if (Salla.cart?.api) Salla.cart.api.addToCartSource = null;
|
|
6
|
+
// Read the clicked list position from session storage (same passing logic as
|
|
7
|
+
// s-viewed-source), falling back to any position carried on the item itself.
|
|
8
|
+
try {
|
|
9
|
+
const position = sessionStorage.getItem('s-viewed-position');
|
|
10
|
+
this.position = position !== null ? Number(position) : (this.item.position ?? null);
|
|
11
|
+
sessionStorage.removeItem('s-viewed-position');
|
|
12
|
+
} catch {
|
|
13
|
+
this.position = this.item.position ?? null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
toSegment() {
|
|
18
|
+
return {
|
|
19
|
+
event: 'Product Added',
|
|
20
|
+
properties: {
|
|
21
|
+
cart_id: this.item.cart_id?.toString(),
|
|
22
|
+
cart_item_id: this.item.cart_item_id?.toString(),
|
|
23
|
+
product_id: this.item.id?.toString(),
|
|
24
|
+
category: this.item.category,
|
|
25
|
+
currency: Salla.config.currency()?.code,
|
|
26
|
+
name: this.item.name,
|
|
27
|
+
brand: this.item.brand,
|
|
28
|
+
variant: this.item.variant?.toString(),
|
|
29
|
+
price: this.item.price,
|
|
30
|
+
quantity: this.item.quantity,
|
|
31
|
+
sku: this.item.sku,
|
|
32
|
+
coupon: this.item.coupon,
|
|
33
|
+
position: this.position,
|
|
34
|
+
url: this.item.url,
|
|
35
|
+
image_url: this.item.image_url,
|
|
36
|
+
source: this.source
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static shouldSend(details) {
|
|
42
|
+
if (salla.config.get('page.slug') === 'customer.wishlist') return false;
|
|
43
|
+
// Suppress cart auto-track for component-attributed adds (pdp_fbt / basket_gap) —
|
|
44
|
+
// FbtProductAdded / BasketGapProductAdded emit those events with their own source.
|
|
45
|
+
if (['pdp_fbt', 'basket_gap'].includes(Salla.cart?.api?.addToCartSource)) {
|
|
46
|
+
if (Salla.cart?.api) Salla.cart.api.addToCartSource = null;
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { mapProductFields } from './product-fields.js';
|
|
2
|
+
|
|
3
|
+
export default class ProductClicked {
|
|
4
|
+
constructor(details) {
|
|
5
|
+
this.item = details[0];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
static shouldSend(details) {
|
|
9
|
+
return !!details?.[0]?.product && !!details?.[0]?.position;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
toSegment() {
|
|
13
|
+
const { product, position } = this.item;
|
|
14
|
+
return {
|
|
15
|
+
event: 'Product Clicked',
|
|
16
|
+
properties: {
|
|
17
|
+
...mapProductFields(product, position),
|
|
18
|
+
source: 'pdp_fbt',
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Shared field mapping for FBT analytics classes.
|
|
2
|
+
export function mapProductFields(product, position, currency = Salla.config.currency()?.code) {
|
|
3
|
+
return {
|
|
4
|
+
product_id: product.id?.toString(),
|
|
5
|
+
name: product.name,
|
|
6
|
+
price: product.price,
|
|
7
|
+
position,
|
|
8
|
+
url: product.url,
|
|
9
|
+
image_url: product.image?.url,
|
|
10
|
+
currency,
|
|
11
|
+
category: product.category,
|
|
12
|
+
brand: product.brand?.name ?? product.brand,
|
|
13
|
+
sku: product.sku,
|
|
14
|
+
};
|
|
15
|
+
}
|