@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,22 @@
|
|
|
1
|
+
|
|
2
|
+
class CouponApplied {
|
|
3
|
+
constructor(details) {
|
|
4
|
+
this.transaction = details;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
toSegment() {
|
|
9
|
+
return {
|
|
10
|
+
event: 'Coupon Applied',
|
|
11
|
+
properties: {
|
|
12
|
+
cart_id: this.transaction.cart_id,
|
|
13
|
+
order_id: this.transaction.order_id,
|
|
14
|
+
coupon_id: this.transaction.coupon_id,
|
|
15
|
+
coupon_name: this.transaction.coupon_name,
|
|
16
|
+
discount: this.transaction.discount,
|
|
17
|
+
currency: this.transaction.currency
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export default CouponApplied;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class CouponDenied {
|
|
2
|
+
constructor(details) {
|
|
3
|
+
this.transaction = details;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
toSegment() {
|
|
7
|
+
return {
|
|
8
|
+
event: 'Coupon Denied',
|
|
9
|
+
properties: {
|
|
10
|
+
cart_id: this.transaction.cart_id,
|
|
11
|
+
order_id: this.transaction.order_id,
|
|
12
|
+
coupon_id: this.transaction.coupon_id,
|
|
13
|
+
coupon_name: this.transaction.coupon_name,
|
|
14
|
+
reason: this.transaction.reason
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default CouponDenied;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
|
|
2
|
+
class CouponEntered {
|
|
3
|
+
constructor(details) {
|
|
4
|
+
this.transaction = details;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
toSegment() {
|
|
8
|
+
return {
|
|
9
|
+
event: 'Coupon Entered',
|
|
10
|
+
properties: {
|
|
11
|
+
cart_id:this.transaction.cart_id,
|
|
12
|
+
order_id: this.transaction.order_id,
|
|
13
|
+
coupon_id: this.transaction.coupon_id,
|
|
14
|
+
value: this.transaction.value,
|
|
15
|
+
currency: this.transaction.currency
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default CouponEntered;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class CouponRemoved {
|
|
2
|
+
constructor(details) {
|
|
3
|
+
this.transaction = details;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
toSegment() {
|
|
7
|
+
return {
|
|
8
|
+
event: 'Coupon Removed',
|
|
9
|
+
properties: {
|
|
10
|
+
cart_id: this.transaction.cart_id,
|
|
11
|
+
order_id: this.transaction.order_id,
|
|
12
|
+
coupon_id: this.transaction.coupon_id,
|
|
13
|
+
coupon_name: this.transaction.coupon_name,
|
|
14
|
+
discount: this.transaction.discount,
|
|
15
|
+
currency: this.transaction.currency
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default CouponRemoved;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import CheckoutStarted from './checkout-started';
|
|
2
|
+
import PaymentInfoEntered from './payment-info-entered';
|
|
3
|
+
import CheckoutStepViewed from './checkout-step-viewed';
|
|
4
|
+
import CheckoutStepCompleted from './checkout-step-completed';
|
|
5
|
+
import CouponEntered from './coupon-entered';
|
|
6
|
+
import CouponApplied from './coupon-applied';
|
|
7
|
+
import CouponDenied from './coupon-denied';
|
|
8
|
+
import CouponRemoved from './coupon-removed';
|
|
9
|
+
import AddressAdded from './address-added';
|
|
10
|
+
import AddressUpdated from './address-updated';
|
|
11
|
+
import MapClicked from './map-clicked';
|
|
12
|
+
import PaymentSubmitted from './payment-submitted';
|
|
13
|
+
import PaymentFailed from './payment-failed';
|
|
14
|
+
import PaymentSucceeded from './payment-succeeded';
|
|
15
|
+
import PaymentPending from './payment-pending';
|
|
16
|
+
|
|
17
|
+
class EventTransformer {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.events = {
|
|
20
|
+
'Checkout Started' : CheckoutStarted,
|
|
21
|
+
'Payment Info Entered' : PaymentInfoEntered,
|
|
22
|
+
'Checkout Step Viewed': CheckoutStepViewed,
|
|
23
|
+
'Checkout Step Completed': CheckoutStepCompleted,
|
|
24
|
+
'Coupon Entered': CouponEntered,
|
|
25
|
+
'Coupon Applied': CouponApplied,
|
|
26
|
+
'Coupon Denied': CouponDenied,
|
|
27
|
+
'Coupon Removed': CouponRemoved,
|
|
28
|
+
'Address Added': AddressAdded,
|
|
29
|
+
'Address Updated': AddressUpdated,
|
|
30
|
+
'Map Clicked': MapClicked,
|
|
31
|
+
'Payment Submitted': PaymentSubmitted,
|
|
32
|
+
'Payment Failed': PaymentFailed,
|
|
33
|
+
'Payment Succeeded': PaymentSucceeded,
|
|
34
|
+
'Payment Pending': PaymentPending,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
transform(eventName, details) {
|
|
39
|
+
const segment = this.events[eventName];
|
|
40
|
+
if (!segment) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
return new segment(details).toSegment();
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error('Failed to transform event:', error);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default new EventTransformer();
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { jitsuAnalytics } from '@jitsu/js';
|
|
2
|
+
import initialData from './initial-data.js';
|
|
3
|
+
import SessionService from './session.js';
|
|
4
|
+
import EventTransformer from './event-transformer.js';
|
|
5
|
+
import { resolveAnalyticsHost } from '../shared/host-resolver.js';
|
|
6
|
+
import { captureSallaCookies } from '../shared/cookie-capture.js';
|
|
7
|
+
import { isValidTracker, replayHistory, pushEventToTrackers } from '../shared/tracker-registry.js';
|
|
8
|
+
import { applyExtraContext } from '../shared/extra-context.js';
|
|
9
|
+
|
|
10
|
+
export default class Analytics {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.history = [];
|
|
13
|
+
this.customTrackers = [];
|
|
14
|
+
this.context = {};
|
|
15
|
+
this.events = [];
|
|
16
|
+
this._sessionId = null;
|
|
17
|
+
this.mappedEvents = {
|
|
18
|
+
'payment::method_updated': 'Payment Info Entered',
|
|
19
|
+
'checkout::step_viewed': 'Checkout Step Viewed',
|
|
20
|
+
'checkout::step_completed': 'Checkout Step Completed',
|
|
21
|
+
'coupon::applied': 'Coupon Applied',
|
|
22
|
+
'coupon::denied': 'Coupon Denied',
|
|
23
|
+
'coupon::entered': 'Coupon Entered',
|
|
24
|
+
'coupon::removed': 'Coupon Removed',
|
|
25
|
+
'address::added': 'Address Added',
|
|
26
|
+
'address::updated': 'Address Updated',
|
|
27
|
+
'map::clicked': 'Map Clicked',
|
|
28
|
+
'payment::submitted': 'Payment Submitted',
|
|
29
|
+
'payment::failed': 'Payment Failed',
|
|
30
|
+
'payment::succeeded': 'Payment Succeeded',
|
|
31
|
+
'payment::pending': 'Payment Pending',
|
|
32
|
+
};
|
|
33
|
+
const config = initialData.get('config.jitsu', {});
|
|
34
|
+
const storeUrl = initialData.get('store.url', null);
|
|
35
|
+
const { host, key : writeKey } = config || {};
|
|
36
|
+
this.analytics = null;
|
|
37
|
+
if (!host || !writeKey) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this.analytics = jitsuAnalytics({
|
|
41
|
+
host: this.getAnalyticsHost(storeUrl),
|
|
42
|
+
writeKey: writeKey,
|
|
43
|
+
cookieCapture: this.captureCookies(),
|
|
44
|
+
fetch: this.customFetch,
|
|
45
|
+
|
|
46
|
+
});
|
|
47
|
+
this.events = initialData.get('config.jitsu.events', []);
|
|
48
|
+
if (initialData.get('config.jitsu.context.extra', {})) {
|
|
49
|
+
this.setExtraContext(initialData.get('config.jitsu.context.extra', {}));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.initSessionId();
|
|
53
|
+
|
|
54
|
+
this.analytics.identify(initialData.get('customer'));
|
|
55
|
+
this.page();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
initSessionId() {
|
|
59
|
+
this._sessionId = SessionService.resolve();
|
|
60
|
+
this.setContext('session_id', this._sessionId);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
captureCookies() {
|
|
64
|
+
return captureSallaCookies();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
getAnalyticsHost(storeUrl) {
|
|
68
|
+
return resolveAnalyticsHost(storeUrl, { log: console.log });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
trackSallaEvent(event) {
|
|
72
|
+
const mappedEventName = this.mappedEvents[event.type] ?? event.type;
|
|
73
|
+
if (!this.analytics || !this.events.includes(mappedEventName)) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
if (!mappedEventName) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const segmentEvent = EventTransformer.transform(mappedEventName, event.detail);
|
|
81
|
+
if(segmentEvent) {
|
|
82
|
+
this.track(segmentEvent.event, segmentEvent.properties);
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.log(error);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
track(eventName, payload = {}) {
|
|
90
|
+
if (!this.analytics) {
|
|
91
|
+
return Promise.resolve();
|
|
92
|
+
}
|
|
93
|
+
payload = this.finalPayload(payload);
|
|
94
|
+
|
|
95
|
+
return this.analytics
|
|
96
|
+
.track(eventName, payload)
|
|
97
|
+
.then((jitsuPayload) => {
|
|
98
|
+
this.pushEventToCustomTrackers('track', eventName, payload);
|
|
99
|
+
return jitsuPayload;
|
|
100
|
+
})
|
|
101
|
+
.then(() => {
|
|
102
|
+
this.history.push({ action: 'track', eventName, payload });
|
|
103
|
+
})
|
|
104
|
+
.catch((error) => {
|
|
105
|
+
return Promise.reject(error);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
page(eventProperties = {}) {
|
|
110
|
+
if (!this.analytics) {
|
|
111
|
+
return Promise.resolve();
|
|
112
|
+
}
|
|
113
|
+
const payload = this.finalPayload(eventProperties);
|
|
114
|
+
return this.analytics
|
|
115
|
+
.page(payload)
|
|
116
|
+
.then((jitsuPayload) => {
|
|
117
|
+
this.pushEventToCustomTrackers('page', payload);
|
|
118
|
+
return jitsuPayload;
|
|
119
|
+
})
|
|
120
|
+
.then(() => {
|
|
121
|
+
this.history.push({ action: 'page', payload });
|
|
122
|
+
})
|
|
123
|
+
.catch((error) => {
|
|
124
|
+
return Promise.reject(error);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
finalPayload(eventProperties = {}) {
|
|
129
|
+
let defaultPayload = {
|
|
130
|
+
// resolve() is sticky (same id all page long) and refreshes the
|
|
131
|
+
// 30-min storage TTL on every event, so an active checkout never
|
|
132
|
+
// rotates its session mid-flow.
|
|
133
|
+
session_id: SessionService.resolve(),
|
|
134
|
+
cart_id: initialData.get('cart.id') || eventProperties.cart_id || eventProperties.checkout_id || null,
|
|
135
|
+
};
|
|
136
|
+
this.setContext('version', initialData.get('config.jitsu.version', 1));
|
|
137
|
+
this.setContext('store_id', initialData.get('config.store.id'));
|
|
138
|
+
if (defaultPayload.session_id) {
|
|
139
|
+
this.setContext('session_id', defaultPayload.session_id);
|
|
140
|
+
}
|
|
141
|
+
//todo:: support passing utms via local storage
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
...defaultPayload,
|
|
145
|
+
...eventProperties,
|
|
146
|
+
session_id: eventProperties.session_id || defaultPayload.session_id,
|
|
147
|
+
cart_id: eventProperties.cart_id || defaultPayload.cart_id,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
registerTracker(tracker) {
|
|
152
|
+
if (!isValidTracker(tracker, this.customTrackers, console.log)) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
this.customTrackers.push(tracker);
|
|
156
|
+
replayHistory(tracker, this.history);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
pushEventToCustomTrackers(action, eventName, payload = {}) {
|
|
160
|
+
return pushEventToTrackers(this.customTrackers, action, eventName, payload, console.log);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
customFetch(url, options) {
|
|
164
|
+
if (!window.navigator?.sendBeacon) {
|
|
165
|
+
return fetch(url, { ...options, keepalive: true });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const beaconUrl = `${url}?writekey=${options?.headers?.['X-Write-Key']}`;
|
|
169
|
+
const body = typeof options?.body === "string" ? options?.body : JSON.stringify(options?.body || {});
|
|
170
|
+
const blob = new Blob([body], { type: "application/json" });
|
|
171
|
+
|
|
172
|
+
const ok = navigator.sendBeacon(beaconUrl, blob);
|
|
173
|
+
if (!ok) {
|
|
174
|
+
// Beacon queue full or payload over quota — keepalive keeps the
|
|
175
|
+
// request alive through page navigation like a beacon would.
|
|
176
|
+
return fetch(url, { ...options, keepalive: true });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return Promise.resolve(new Response('{"status":"sent_via_beacon"}', {
|
|
180
|
+
status: 200,
|
|
181
|
+
headers: { "Content-Type": "application/json" }
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
setContext(key, value){
|
|
186
|
+
this.analytics?.setContextProperty(key, value);
|
|
187
|
+
this.context[key] = value;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
setExtraContext(extra){
|
|
191
|
+
applyExtraContext((key, value) => this.setContext(key, value), extra, console.log);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export { default as SessionService } from './session.js';
|
|
196
|
+
export { default as initialData } from './initial-data.js';
|
|
197
|
+
export { sanitizeErrorMessage, sanitizeFieldErrors } from './sanitizers.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dotted-path reader over the server-rendered `window.initialData` payload
|
|
3
|
+
* (rendered by the checkout blade views). Single source of truth for the
|
|
4
|
+
* initial-data access the analytics layer needs.
|
|
5
|
+
*/
|
|
6
|
+
export default {
|
|
7
|
+
get(key, defaultValue = null) {
|
|
8
|
+
const initialData = typeof window !== 'undefined' ? window.initialData : null;
|
|
9
|
+
if (!initialData || typeof initialData !== 'object') {
|
|
10
|
+
return defaultValue;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let result = initialData;
|
|
14
|
+
for (const keyPart of key.split('.')) {
|
|
15
|
+
if (result === null || result === undefined || typeof result !== 'object' || !(keyPart in result)) {
|
|
16
|
+
return defaultValue;
|
|
17
|
+
}
|
|
18
|
+
result = result[keyPart];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return result !== undefined ? result : defaultValue;
|
|
22
|
+
},
|
|
23
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export default class MapClicked {
|
|
2
|
+
constructor(details) {
|
|
3
|
+
this.transaction = details;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
toSegment() {
|
|
7
|
+
return {
|
|
8
|
+
event: 'Map Clicked',
|
|
9
|
+
properties: {
|
|
10
|
+
checkout_id: this.transaction.cart_id,
|
|
11
|
+
order_id: this.transaction.order_id,
|
|
12
|
+
cart_id: this.transaction.cart_id,
|
|
13
|
+
customer: {
|
|
14
|
+
id: this.transaction.customer?.id,
|
|
15
|
+
name: this.transaction.customer?.first_name + ' ' + this.transaction.customer?.last_name,
|
|
16
|
+
mobile: this.transaction.customer?.mobile,
|
|
17
|
+
email: this.transaction.customer?.email,
|
|
18
|
+
gender: this.transaction.customer?.gender,
|
|
19
|
+
},
|
|
20
|
+
address : {
|
|
21
|
+
lat : this.transaction.address?.lat,
|
|
22
|
+
lng : this.transaction.address?.lng,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import initialData from './initial-data.js';
|
|
2
|
+
import { sanitizeErrorMessage, sanitizeFieldErrors } from './sanitizers.js';
|
|
3
|
+
|
|
4
|
+
export default class PaymentFailed {
|
|
5
|
+
constructor(details) {
|
|
6
|
+
this.details = details || {};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
toSegment() {
|
|
10
|
+
const errorFields = this.details.error_fields || {};
|
|
11
|
+
const fieldNames = errorFields && typeof errorFields === 'object' ? Object.keys(errorFields) : [];
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
event: 'Payment Failed',
|
|
15
|
+
properties: {
|
|
16
|
+
checkout_id: this.details.checkout_id || initialData.get('cart.id'),
|
|
17
|
+
payment_method: this.details.payment_method,
|
|
18
|
+
gateway: this.details.gateway,
|
|
19
|
+
error_code: this.details.error_code,
|
|
20
|
+
error_type: this.details.error_type,
|
|
21
|
+
error_message: sanitizeErrorMessage(this.details.error_message),
|
|
22
|
+
http_status: this.details.http_status,
|
|
23
|
+
error_fields: fieldNames,
|
|
24
|
+
field_errors: sanitizeFieldErrors(errorFields),
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import initialData from './initial-data.js';
|
|
2
|
+
export default class PaymentInfoEntered {
|
|
3
|
+
constructor(details) {
|
|
4
|
+
this.transaction = details;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
toSegment() {
|
|
8
|
+
return {
|
|
9
|
+
event: 'Payment Info Entered',
|
|
10
|
+
properties: {
|
|
11
|
+
order_id: this.transaction.order_id,
|
|
12
|
+
payment_method: this.transaction.payment_method,
|
|
13
|
+
checkout_id: this.transaction.checkout_id,
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import initialData from './initial-data.js';
|
|
2
|
+
|
|
3
|
+
export default class PaymentPending {
|
|
4
|
+
constructor(details) {
|
|
5
|
+
this.details = details || {};
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
toSegment() {
|
|
9
|
+
return {
|
|
10
|
+
event: 'Payment Pending',
|
|
11
|
+
properties: {
|
|
12
|
+
checkout_id: this.details.checkout_id || initialData.get('cart.id'),
|
|
13
|
+
payment_method: this.details.payment_method,
|
|
14
|
+
gateway: this.details.gateway,
|
|
15
|
+
redirect_url: this.details.redirect_url || null,
|
|
16
|
+
order_id: this.details.order_id || null,
|
|
17
|
+
cart_total: this.details.cart_total || initialData.get('cart.total_in_customer_currency', 0),
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import initialData from './initial-data.js';
|
|
2
|
+
|
|
3
|
+
export default class PaymentSubmitted {
|
|
4
|
+
constructor(details) {
|
|
5
|
+
this.details = details || {};
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
toSegment() {
|
|
9
|
+
return {
|
|
10
|
+
event: 'Payment Submitted',
|
|
11
|
+
properties: {
|
|
12
|
+
checkout_id: this.details.checkout_id || initialData.get('cart.id'),
|
|
13
|
+
payment_method: this.details.payment_method,
|
|
14
|
+
gateway: this.details.gateway,
|
|
15
|
+
is_order_free: this.details.is_order_free,
|
|
16
|
+
cart_total: this.details.cart_total || initialData.get('cart.total_in_customer_currency', 0),
|
|
17
|
+
shipping_method: this.details.shipping_method,
|
|
18
|
+
has_partial_payment: this.details.has_partial_payment || false,
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import initialData from './initial-data.js';
|
|
2
|
+
|
|
3
|
+
export default class PaymentSucceeded {
|
|
4
|
+
constructor(details) {
|
|
5
|
+
this.details = details || {};
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
toSegment() {
|
|
9
|
+
return {
|
|
10
|
+
event: 'Payment Succeeded',
|
|
11
|
+
properties: {
|
|
12
|
+
checkout_id: this.details.checkout_id || initialData.get('cart.id'),
|
|
13
|
+
payment_method: this.details.payment_method,
|
|
14
|
+
gateway: this.details.gateway,
|
|
15
|
+
response_status: this.details.response_status,
|
|
16
|
+
has_redirect: this.details.has_redirect || false,
|
|
17
|
+
redirect_url: this.details.redirect_url || null,
|
|
18
|
+
order_id: this.details.order_id || null,
|
|
19
|
+
cart_total: this.details.cart_total || initialData.get('cart.total_in_customer_currency', 0),
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const PAN_REGEX = /\b(?:\d[ -]?){13,19}\b/g;
|
|
2
|
+
const EMAIL_REGEX = /[\w.+-]+@[\w-]+(\.[\w-]+)+/g;
|
|
3
|
+
const MAX_MESSAGE_LENGTH = 500;
|
|
4
|
+
|
|
5
|
+
/** Redact card numbers / emails from analytics error messages and cap length. */
|
|
6
|
+
export const sanitizeErrorMessage = (message) => {
|
|
7
|
+
if (!message) return '';
|
|
8
|
+
return String(message)
|
|
9
|
+
.replace(PAN_REGEX, '[REDACTED]')
|
|
10
|
+
.replace(EMAIL_REGEX, '[REDACTED]')
|
|
11
|
+
.slice(0, MAX_MESSAGE_LENGTH);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const sanitizeFieldErrors = (errorFields) => {
|
|
15
|
+
if (!errorFields || typeof errorFields !== 'object') return [];
|
|
16
|
+
return Object.keys(errorFields).map(field => {
|
|
17
|
+
const raw = Array.isArray(errorFields[field]) ? errorFields[field][0] : errorFields[field];
|
|
18
|
+
const message = raw && typeof raw === 'object' ? (raw.message || JSON.stringify(raw)) : raw;
|
|
19
|
+
return { field, message: sanitizeErrorMessage(message) };
|
|
20
|
+
});
|
|
21
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import initialData from './initial-data.js';
|
|
2
|
+
import { generateSessionId } from '../shared/uuid.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SessionService
|
|
6
|
+
*
|
|
7
|
+
* Provides a tracking session id that is shared with the storefront (twilight).
|
|
8
|
+
*
|
|
9
|
+
* twilight persists the session id via `Salla.storage.setWithTTL('s-session-id', id, 30)`,
|
|
10
|
+
* which — because the store-engine is created without a namespace — writes a plain
|
|
11
|
+
* localStorage entry:
|
|
12
|
+
*
|
|
13
|
+
* key: `s-session-id_<storeId>`
|
|
14
|
+
* value: `{"value":"<uuid>","expiry":<epochMillis>}`
|
|
15
|
+
*
|
|
16
|
+
* Checkout runs on the same origin as the storefront, so by reading/writing the exact
|
|
17
|
+
* same key + shape we continue the same session instead of starting a new one. No Salla
|
|
18
|
+
* SDK is required here — we only need plain localStorage and the store id.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const SESSION_STORAGE_KEY = 's-session-id';
|
|
22
|
+
const SESSION_TTL_MINUTES = 30;
|
|
23
|
+
|
|
24
|
+
// Memoized per page load — see resolve().
|
|
25
|
+
let resolvedSessionId = null;
|
|
26
|
+
|
|
27
|
+
// Mirrors twilight's Storage.prefixKey: `${key}_${Salla.config.get('store.id')}`.
|
|
28
|
+
function prefixKey(key) {
|
|
29
|
+
return `${key}_${initialData.get('store.id')}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getWithTTL(key, defaultValue = null) {
|
|
33
|
+
try {
|
|
34
|
+
const raw = localStorage.getItem(prefixKey(key));
|
|
35
|
+
if (!raw) {
|
|
36
|
+
return defaultValue;
|
|
37
|
+
}
|
|
38
|
+
const item = JSON.parse(raw);
|
|
39
|
+
if (!item || typeof item !== 'object') {
|
|
40
|
+
return defaultValue;
|
|
41
|
+
}
|
|
42
|
+
if (new Date().getTime() > item.expiry) {
|
|
43
|
+
localStorage.removeItem(prefixKey(key));
|
|
44
|
+
return defaultValue;
|
|
45
|
+
}
|
|
46
|
+
return item.value;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return defaultValue;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function setWithTTL(key, value, ttlInMinutes = SESSION_TTL_MINUTES) {
|
|
53
|
+
try {
|
|
54
|
+
const expiry = new Date().getTime() + ttlInMinutes * 60 * 1000;
|
|
55
|
+
localStorage.setItem(prefixKey(key), JSON.stringify({ value, expiry }));
|
|
56
|
+
} catch (error) {
|
|
57
|
+
// localStorage may be unavailable (private mode / disabled) — fail silently.
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default {
|
|
62
|
+
SESSION_STORAGE_KEY,
|
|
63
|
+
SESSION_TTL_MINUTES,
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolve the tracking session id.
|
|
67
|
+
*
|
|
68
|
+
* Continues the storefront session when one exists in the shared (same-origin)
|
|
69
|
+
* localStorage; otherwise starts a new one. Either way the resolved id is written
|
|
70
|
+
* back with a fresh 30-minute sliding window, so an active session stays alive and
|
|
71
|
+
* the storefront picks up the same id on the next navigation.
|
|
72
|
+
*/
|
|
73
|
+
resolve() {
|
|
74
|
+
if (!resolvedSessionId) {
|
|
75
|
+
resolvedSessionId = getWithTTL(SESSION_STORAGE_KEY) || generateSessionId();
|
|
76
|
+
}
|
|
77
|
+
setWithTTL(SESSION_STORAGE_KEY, resolvedSessionId);
|
|
78
|
+
return resolvedSessionId;
|
|
79
|
+
},
|
|
80
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { default as StorefrontAnalytics } from './storefront/index.js';
|
|
2
|
+
export { default as CheckoutAnalytics } from './checkout/index.js';
|
|
3
|
+
export { resolveAnalyticsHost, FALLBACK_ANALYTICS_HOST } from './shared/host-resolver.js';
|
|
4
|
+
export { captureSallaCookies } from './shared/cookie-capture.js';
|
|
5
|
+
export { isValidTracker, replayHistory, pushEventToTrackers } from './shared/tracker-registry.js';
|
|
6
|
+
export { applyExtraContext } from './shared/extra-context.js';
|
|
7
|
+
export { generateSessionId } from './shared/uuid.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collect cookie names with the given prefix in the {name: name} shape Jitsu's
|
|
3
|
+
* cookieCapture option expects (Jitsu reads the values itself).
|
|
4
|
+
*/
|
|
5
|
+
export function captureSallaCookies(prefix = 'sa_') {
|
|
6
|
+
try {
|
|
7
|
+
return document.cookie.split(';').reduce((acc, cookie) => {
|
|
8
|
+
const [name] = cookie.trim().split('=');
|
|
9
|
+
if (name.startsWith(prefix)) {
|
|
10
|
+
acc[name] = name;
|
|
11
|
+
}
|
|
12
|
+
return acc;
|
|
13
|
+
}, {});
|
|
14
|
+
} catch (error) {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map the server-provided `config.jitsu.context.extra` node (s-* request
|
|
3
|
+
* headers, release version) onto Jitsu context: mobile-app headers become the
|
|
4
|
+
* `app` context, the remainder becomes `system`.
|
|
5
|
+
*/
|
|
6
|
+
export function applyExtraContext(setContext, extra, log = () => {}) {
|
|
7
|
+
try {
|
|
8
|
+
if (extra?.headers?.['s-app-os']) {
|
|
9
|
+
setContext('app', {
|
|
10
|
+
version: extra.headers['s-app-os-version'],
|
|
11
|
+
app_release_tag: extra.headers['s-app-version'],
|
|
12
|
+
platform: extra.headers['s-app-os'].toLowerCase()
|
|
13
|
+
});
|
|
14
|
+
delete extra.headers['s-app-os'];
|
|
15
|
+
delete extra.headers['s-app-os-version'];
|
|
16
|
+
delete extra.headers['s-app-version'];
|
|
17
|
+
delete extra.headers['s-source'];
|
|
18
|
+
}
|
|
19
|
+
setContext('system', extra);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
log('Error applying extra analytics context:', error);
|
|
22
|
+
}
|
|
23
|
+
}
|