@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,58 @@
|
|
|
1
|
+
export const FALLBACK_ANALYTICS_HOST = 'https://salla.cloud/_app/e';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the per-store analytics ingestion host from the store URL.
|
|
5
|
+
*
|
|
6
|
+
* - salla.sa stores (https://salla.sa/[ar|en/]store-name) -> origin/store-name/_app/e
|
|
7
|
+
* - demostore.salla.sa -> origin/store-name/_app/e (falls back when no store name)
|
|
8
|
+
* - custom domains (incl. multi-part TLDs like co.uk, com.sa) -> origin/_app/e
|
|
9
|
+
* - inside an iframe the configured host wins (same-origin proxying is impossible)
|
|
10
|
+
*
|
|
11
|
+
* @param {string|null} storeUrl the store's public URL
|
|
12
|
+
* @param {object} [options]
|
|
13
|
+
* @param {boolean} [options.iframe] running inside an iframe (theme preview)
|
|
14
|
+
* @param {string|null} [options.iframeHost] host to use when iframe is true
|
|
15
|
+
* @param {function} [options.log] logger for resolution failures
|
|
16
|
+
* @returns {string} analytics ingestion endpoint
|
|
17
|
+
*/
|
|
18
|
+
export function resolveAnalyticsHost(storeUrl, { iframe = false, iframeHost = null, log = () => {} } = {}) {
|
|
19
|
+
if (iframe) {
|
|
20
|
+
return iframeHost ?? FALLBACK_ANALYTICS_HOST;
|
|
21
|
+
}
|
|
22
|
+
if (!storeUrl) {
|
|
23
|
+
log('Error getting store url - no store URL provided');
|
|
24
|
+
return FALLBACK_ANALYTICS_HOST;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// demostore.salla.sa - extract store name and use it
|
|
29
|
+
if (/https:\/\/demostore\.salla\.sa/.test(storeUrl)) {
|
|
30
|
+
const demostoreMatch = storeUrl.match(/demostore\.salla\.sa\/(?:(ar|en)\/)?((?!ar$|en$)[a-zA-Z0-9.-]+)/);
|
|
31
|
+
if (demostoreMatch && demostoreMatch[2]) {
|
|
32
|
+
const parsed = new URL(storeUrl);
|
|
33
|
+
return `${parsed.origin}/${demostoreMatch[2]}/_app/e`;
|
|
34
|
+
}
|
|
35
|
+
return FALLBACK_ANALYTICS_HOST;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// salla.sa domains: https://salla.sa/greenland9 or https://salla.sa/ar/greenland9
|
|
39
|
+
const sallaPattern = /(salla\.sa)\/(?:(ar|en)\/)?((?!ar$|en$)[a-zA-Z0-9.-]+)/;
|
|
40
|
+
const sallaMatch = storeUrl.match(sallaPattern);
|
|
41
|
+
|
|
42
|
+
if (sallaMatch) {
|
|
43
|
+
const storeName = sallaMatch[3];
|
|
44
|
+
if (!storeName || storeName.trim().length === 0 || storeName === 'ar' || storeName === 'en') {
|
|
45
|
+
log('Error: invalid or empty store name in salla.sa URL');
|
|
46
|
+
return FALLBACK_ANALYTICS_HOST;
|
|
47
|
+
}
|
|
48
|
+
const parsed = new URL(storeUrl);
|
|
49
|
+
return `${parsed.origin}/${storeName}/_app/e`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const parsed = new URL(storeUrl);
|
|
53
|
+
return `${parsed.origin}/_app/e`;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
log('Error getting store url, forwarding to salla.cloud:', error);
|
|
56
|
+
return FALLBACK_ANALYTICS_HOST;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom tracker registry shared by the storefront and checkout engines.
|
|
3
|
+
* A tracker is {name: string, track: fn(eventName, payload), page: fn(payload)}.
|
|
4
|
+
*/
|
|
5
|
+
export function isValidTracker(tracker, existingTrackers, log = () => {}) {
|
|
6
|
+
if (typeof tracker !== 'object' || tracker === null) {
|
|
7
|
+
log('tracker most be an object {name:"tracker name", track: callback, page: callback}!');
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
if (typeof tracker.name !== 'string' || !tracker.name) {
|
|
11
|
+
log('tracker.name most be a string!');
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (typeof tracker.track !== 'function') {
|
|
15
|
+
log('tracker.track most be a function!');
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
if (typeof tracker.page !== 'function') {
|
|
19
|
+
log('tracker.page most be a function!');
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
if (existingTrackers.find((t) => t.name === tracker.name)) {
|
|
23
|
+
log('tracker with the same name already exists!');
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Replay already-sent history to a newly registered tracker. */
|
|
30
|
+
export function replayHistory(tracker, history) {
|
|
31
|
+
history.forEach(event => {
|
|
32
|
+
if (event.action === 'track') {
|
|
33
|
+
return tracker.track(event.eventName, event.payload);
|
|
34
|
+
}
|
|
35
|
+
if (event.action === 'page') {
|
|
36
|
+
return tracker.page(event.payload);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Fan an event out to every tracker; a failing tracker never breaks the others. */
|
|
42
|
+
export function pushEventToTrackers(trackers, action, eventName, payload = {}, log = () => {}) {
|
|
43
|
+
const args = action === 'page' ? [eventName] : [eventName, payload];
|
|
44
|
+
return Promise.allSettled(trackers.map(tracker => pushEventToTracker(tracker[action], args)))
|
|
45
|
+
.then(results => results.filter(result => result.status === 'rejected').map(result => log('Error on customTracker to send ' + action, result)));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function pushEventToTracker(customTrackerCallback, args) {
|
|
49
|
+
try {
|
|
50
|
+
const result = customTrackerCallback(...args);
|
|
51
|
+
return Promise.resolve(result).catch(error => Promise.reject(error));
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return Promise.reject(error);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function generateSessionId() {
|
|
2
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
3
|
+
return crypto.randomUUID();
|
|
4
|
+
}
|
|
5
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
6
|
+
const r = (Math.random() * 16) | 0;
|
|
7
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
8
|
+
return v.toString(16);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { mapProductFields } from './product-fields.js';
|
|
2
|
+
|
|
3
|
+
export default class BasketGapProductAdded {
|
|
4
|
+
constructor(details) {
|
|
5
|
+
this.item = details[0];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
toSegment() {
|
|
9
|
+
const { product, position, cartItem, cartId } = this.item;
|
|
10
|
+
return {
|
|
11
|
+
event: 'Product Added',
|
|
12
|
+
properties: {
|
|
13
|
+
...mapProductFields(product, position),
|
|
14
|
+
cart_id: cartId?.toString(),
|
|
15
|
+
cart_item_id: cartItem?.id?.toString(),
|
|
16
|
+
variant: cartItem?.variant?.toString() ?? null,
|
|
17
|
+
coupon: cartItem?.coupon ?? null,
|
|
18
|
+
quantity: cartItem?.quantity ?? 1,
|
|
19
|
+
source: 'basket_gap',
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Identifies basket-gap component emits by their explicit source marker.
|
|
25
|
+
// Falls through to FbtProductAdded/ProductAdded for other 'Product Added' emits.
|
|
26
|
+
static shouldSend(details) {
|
|
27
|
+
return details?.[0]?.source === 'basket_gap';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export default class CartUpdated {
|
|
2
|
+
constructor(details) {
|
|
3
|
+
this.cart = details;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
toSegment() {
|
|
7
|
+
return {
|
|
8
|
+
event: 'Cart Updated',
|
|
9
|
+
properties: {
|
|
10
|
+
cart_id: this.cart.id?.toString(),
|
|
11
|
+
value: this.cart.total,
|
|
12
|
+
revenue: this.cart.sub_total,
|
|
13
|
+
tax: this.cart.tax_amount,
|
|
14
|
+
discount: this.cart.total_discount,
|
|
15
|
+
shipping: this.cart.real_shipping_cost,
|
|
16
|
+
currency: Salla.config.currency()?.code,
|
|
17
|
+
affiliation: Salla.storage.getWithTTL('s-utms')?.['s-utm-source'],
|
|
18
|
+
products: this.cart.items?.map(item => ({
|
|
19
|
+
product_id: item.product_id?.toString(),
|
|
20
|
+
name: item.product_name,
|
|
21
|
+
price: item.product_price,
|
|
22
|
+
category: item.category?.toString(), // not supported
|
|
23
|
+
sku: item.product_sku?.toString(),
|
|
24
|
+
position: item.position,
|
|
25
|
+
url: item.url,
|
|
26
|
+
image_url: item.product_image,
|
|
27
|
+
brand: item.brand?.toString(),// not supported
|
|
28
|
+
currency: Salla.config.currency()?.code,
|
|
29
|
+
variant: item.variant?.toString(),//not supported
|
|
30
|
+
coupon: this.cart.coupon,
|
|
31
|
+
quantity: item.quantity,
|
|
32
|
+
}))
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static shouldSend(details) {
|
|
39
|
+
return salla.config.get('page.slug') === 'cart' && !details?.fromLatestFetched;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export default class CartViewed {
|
|
2
|
+
constructor(details) {
|
|
3
|
+
this.cart = details[0];
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
toSegment() {
|
|
7
|
+
return {
|
|
8
|
+
event: 'Cart Viewed',
|
|
9
|
+
properties: {
|
|
10
|
+
cart_id: this.cart.cart_id?.toString(),
|
|
11
|
+
products: this.cart.products.map(product => ({
|
|
12
|
+
product_id: product.id?.toString(),
|
|
13
|
+
cart_item_id: product.cart_item_id?.toString(),
|
|
14
|
+
name: product.name,
|
|
15
|
+
price: product.price,
|
|
16
|
+
category: product.category,
|
|
17
|
+
sku: product.sku,
|
|
18
|
+
position: product.position,
|
|
19
|
+
url: product.url,
|
|
20
|
+
image_url: product.image_url,
|
|
21
|
+
brand: product.brand,
|
|
22
|
+
currency: Salla.config.currency()?.code,
|
|
23
|
+
variant: product.variant,//todo:: now it's the sku id, we need to be string (option Details names) such: Red/L/Circle
|
|
24
|
+
coupon: product.coupon,
|
|
25
|
+
quantity: product.quantity,
|
|
26
|
+
}))
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class CouponApplied {
|
|
2
|
+
constructor(details) {
|
|
3
|
+
this.transaction = details;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
getCartId() {
|
|
7
|
+
if (salla.config.get('page.slug') === 'cart') {
|
|
8
|
+
return salla.config.get('page.id')?.toString();
|
|
9
|
+
}
|
|
10
|
+
return this.transaction?.data?.cart?.id?.toString() || '';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
toSegment() {
|
|
14
|
+
return {
|
|
15
|
+
event: 'Coupon Applied',
|
|
16
|
+
properties: {
|
|
17
|
+
cart_id: this.getCartId(),
|
|
18
|
+
coupon_name: this.transaction?.data?.cart?.coupon || '',
|
|
19
|
+
discount: this.transaction?.data?.cart?.discount || 0,
|
|
20
|
+
currency: salla.config.get('user.currency') || ''
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default CouponApplied;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
|
|
2
|
+
class CouponDenied {
|
|
3
|
+
constructor(details) {
|
|
4
|
+
this.transaction = details;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
getCartId() {
|
|
8
|
+
if (salla.config.get('page.slug') === 'cart') {
|
|
9
|
+
return salla.config.get('page.id')?.toString();
|
|
10
|
+
}
|
|
11
|
+
return '';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
getCouponCode() {
|
|
15
|
+
try {
|
|
16
|
+
const data = this.transaction.response?.config?.data;
|
|
17
|
+
return data ? JSON.parse(data)?.coupon : '';
|
|
18
|
+
} catch {
|
|
19
|
+
return '';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
toSegment() {
|
|
24
|
+
return {
|
|
25
|
+
event: 'Coupon Denied',
|
|
26
|
+
properties: {
|
|
27
|
+
cart_id: this.getCartId(),
|
|
28
|
+
coupon_name: this.getCouponCode(),
|
|
29
|
+
reason: this.transaction.response?.data?.error?.message || 'Unknown error'
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default CouponDenied;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
|
|
2
|
+
class CouponRemoved {
|
|
3
|
+
constructor(details) {
|
|
4
|
+
this.transaction = details;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
getCartId() {
|
|
8
|
+
if (salla.config.get('page.slug') === 'cart') {
|
|
9
|
+
return salla.config.get('page.id')?.toString();
|
|
10
|
+
}
|
|
11
|
+
return this.transaction?.data?.cart?.id?.toString() || '';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
toSegment() {
|
|
15
|
+
return {
|
|
16
|
+
event: 'Coupon Removed',
|
|
17
|
+
properties: {
|
|
18
|
+
cart_id: this.getCartId(),
|
|
19
|
+
coupon_name: this.transaction?.coupon || '',
|
|
20
|
+
discount: this.transaction?.data?.cart?.discount || 0,
|
|
21
|
+
currency: salla.config.get('user.currency') || ''
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default CouponRemoved;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import ProductAdded from './product-added';
|
|
2
|
+
import RecommendationListViewed from './recommendation-list-viewed';
|
|
3
|
+
import ProductClicked from './product-clicked';
|
|
4
|
+
import FbtProductAdded from './fbt-product-added';
|
|
5
|
+
import BasketGapProductAdded from './basket-gap-product-added';
|
|
6
|
+
import ProductViewed from './product-viewed';
|
|
7
|
+
import CartViewed from './cart-viewed';
|
|
8
|
+
import ProductListViewed from './product-list-viewed';
|
|
9
|
+
import ProductsSearched from './products-searched';
|
|
10
|
+
import SignedUp from './signed-up';
|
|
11
|
+
import SignedIn from './signed-in';
|
|
12
|
+
import SignedOut from './signed-out';
|
|
13
|
+
import ProductRemoved from './product-removed';
|
|
14
|
+
import ProductAddedToWishlist from './product-added-to-wishlist';
|
|
15
|
+
import ProductRemovedFromWishlist from './product-removed-from-wishlist';
|
|
16
|
+
import OrderCompleted from './order-completed';
|
|
17
|
+
import WishlistProductAddedToCart from './wishlist-product-added-to-cart';
|
|
18
|
+
import ProductShared from './product-shared';
|
|
19
|
+
import ProductReviewed from './product-reviewed';
|
|
20
|
+
import PromotionViewed from './promotion-viewed';
|
|
21
|
+
import PromotionClicked from './promotion-clicked';
|
|
22
|
+
import CouponApplied from './coupon-applied';
|
|
23
|
+
import CouponDenied from './coupon-denied';
|
|
24
|
+
import CouponRemoved from './coupon-removed';
|
|
25
|
+
import UserProfileUpdated from './user-profile-updated';
|
|
26
|
+
import ProductListFiltered from './product-list-filtered';
|
|
27
|
+
import CartUpdated from './cart-updated';
|
|
28
|
+
import MarketSelected from './market-selected';
|
|
29
|
+
import ProductReordered from './product-reordered';
|
|
30
|
+
|
|
31
|
+
export default class EventTransformer {
|
|
32
|
+
static events = {
|
|
33
|
+
'Order Completed': [OrderCompleted],
|
|
34
|
+
'Product Added': [BasketGapProductAdded, FbtProductAdded, ProductAdded, WishlistProductAddedToCart],
|
|
35
|
+
'Product Removed': [ProductRemoved],
|
|
36
|
+
'Product Viewed': [ProductViewed],
|
|
37
|
+
'Product Added to Wishlist': [ProductAddedToWishlist],
|
|
38
|
+
'Product Removed from Wishlist': [ProductRemovedFromWishlist],
|
|
39
|
+
'Cart Viewed': [CartViewed],
|
|
40
|
+
'Products Searched': [ProductsSearched],
|
|
41
|
+
'Product List Viewed': [ProductListViewed],
|
|
42
|
+
'Product List Filtered': [ProductListFiltered],
|
|
43
|
+
'Signed Up': [SignedUp],
|
|
44
|
+
'Signed In': [SignedIn],
|
|
45
|
+
'Signed Out': [SignedOut],
|
|
46
|
+
'Product Reviewed': [ProductReviewed],
|
|
47
|
+
'Product Shared': [ProductShared],
|
|
48
|
+
'Promotion Viewed': [PromotionViewed],
|
|
49
|
+
'Promotion Clicked': [PromotionClicked],
|
|
50
|
+
'Coupon Applied': [CouponApplied],
|
|
51
|
+
'Coupon Removed': [CouponRemoved],
|
|
52
|
+
'Coupon Denied': [CouponDenied],
|
|
53
|
+
'User Profile Updated': [UserProfileUpdated],
|
|
54
|
+
'Cart Updated': [CartUpdated],
|
|
55
|
+
'Market Selected': [MarketSelected],
|
|
56
|
+
'Product Reordered': [ProductReordered],
|
|
57
|
+
'Recommendation List Viewed': [RecommendationListViewed],
|
|
58
|
+
'Product Clicked': [ProductClicked],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
static registerEvents() {
|
|
62
|
+
Object.values(this.events).forEach(eventHandlers => {
|
|
63
|
+
eventHandlers.forEach(eventClass => {
|
|
64
|
+
if (typeof eventClass.registerEvent === 'function') {
|
|
65
|
+
eventClass.registerEvent();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
static transform(eventName, details) {
|
|
72
|
+
try {
|
|
73
|
+
// Get the array of event handlers for this event name
|
|
74
|
+
const eventHandlers = this.events[eventName] || [];
|
|
75
|
+
for (const eventClass of eventHandlers) {
|
|
76
|
+
if (!eventClass ||
|
|
77
|
+
(typeof eventClass.shouldSend === 'function' && !eventClass.shouldSend(details))) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const result = new eventClass(details).toSegment();
|
|
81
|
+
return { event: result.event, properties: result.properties };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return null;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error('Failed to transform event:', error);
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { mapProductFields } from './product-fields.js';
|
|
2
|
+
|
|
3
|
+
export default class FbtProductAdded {
|
|
4
|
+
constructor(details) {
|
|
5
|
+
this.item = details[0];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
toSegment() {
|
|
9
|
+
const { product, position, cartItem, cartId } = this.item;
|
|
10
|
+
return {
|
|
11
|
+
event: 'Product Added',
|
|
12
|
+
properties: {
|
|
13
|
+
...mapProductFields(product, position),
|
|
14
|
+
cart_id: cartId?.toString(),
|
|
15
|
+
cart_item_id: cartItem?.id?.toString(),
|
|
16
|
+
variant: cartItem?.variant?.toString() ?? null,
|
|
17
|
+
coupon: cartItem?.coupon ?? null,
|
|
18
|
+
quantity: cartItem?.quantity ?? 1,
|
|
19
|
+
source: 'pdp_fbt',
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Identifies FBT component emits by their { product, position, cartItem, cartId } shape.
|
|
25
|
+
// Cart auto-track emits a flat product object — no .product key — so it falls through to ProductAdded.
|
|
26
|
+
static shouldSend(details) {
|
|
27
|
+
return !!details?.[0]?.product;
|
|
28
|
+
}
|
|
29
|
+
}
|