medusa-storefront-analytics 1.2.0 → 1.4.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/dist/api/meta-event.d.ts +19 -0
- package/dist/index.d.ts +2 -0
- package/dist/ui-library.js +4861 -290
- package/dist/ui-library.umd.cjs +41 -1
- package/package.json +66 -39
- package/src/api/meta-event.ts +88 -0
- package/src/core/compose.ts +24 -0
- package/src/core/noop-adapter.ts +8 -0
- package/src/core/types.ts +35 -0
- package/src/ecommerce/ga4-ecommerce.ts +261 -0
- package/src/events/forms.ts +34 -0
- package/src/events/meta-events.ts +31 -0
- package/src/events/reviews.ts +25 -0
- package/src/index.ts +74 -0
- package/src/providers/gtm.ts +97 -0
- package/src/providers/meta-pixel-guard.ts +96 -0
- package/src/providers/meta-pixel.ts +77 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Shared product reference for ecommerce / UGC events */
|
|
2
|
+
export interface ProductRef {
|
|
3
|
+
productId: string;
|
|
4
|
+
productName: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface ReviewSubmittedPayload extends ProductRef {
|
|
8
|
+
rating: number;
|
|
9
|
+
/** Review title entered by the customer */
|
|
10
|
+
reviewTitle?: string;
|
|
11
|
+
/** True when an existing review was updated */
|
|
12
|
+
isUpdate: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface FormSubmittedPayload {
|
|
16
|
+
form_type: string;
|
|
17
|
+
formType?: string;
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Port implemented by each site (GTM, Meta, custom). Domain helpers call these methods.
|
|
23
|
+
*/
|
|
24
|
+
export interface StorefrontAnalyticsAdapter {
|
|
25
|
+
trackReviewSubmitted(payload: ReviewSubmittedPayload): void;
|
|
26
|
+
trackReviewUpdated?(payload: ReviewSubmittedPayload): void;
|
|
27
|
+
trackFormSubmitted?(payload: FormSubmittedPayload): void;
|
|
28
|
+
trackEvent?(eventName: string, data?: Record<string, unknown>): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type AnalyticsEventMap = {
|
|
32
|
+
review_submitted: ReviewSubmittedPayload;
|
|
33
|
+
review_updated: ReviewSubmittedPayload;
|
|
34
|
+
form_submit: FormSubmittedPayload;
|
|
35
|
+
};
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GA4 dataLayer ecommerce helpers + Meta Pixel (explicit fbq).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
fireMetaEcommerceEvent,
|
|
7
|
+
fireMetaPixelEvent,
|
|
8
|
+
fireMetaPurchase,
|
|
9
|
+
} from "../providers/meta-pixel";
|
|
10
|
+
import {
|
|
11
|
+
META_STANDARD_EVENTS,
|
|
12
|
+
type EcommercePayload,
|
|
13
|
+
type GA4Item,
|
|
14
|
+
type MetaStandardEventName,
|
|
15
|
+
} from "../events/meta-events";
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
META_STANDARD_EVENTS,
|
|
19
|
+
type EcommercePayload,
|
|
20
|
+
type GA4Item,
|
|
21
|
+
type MetaStandardEventName,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const GA4_TO_META_EVENT: Record<string, MetaStandardEventName> = {
|
|
25
|
+
add_to_cart: META_STANDARD_EVENTS.AddToCart,
|
|
26
|
+
view_item: META_STANDARD_EVENTS.ViewContent,
|
|
27
|
+
view_cart: META_STANDARD_EVENTS.ViewContent,
|
|
28
|
+
begin_checkout: META_STANDARD_EVENTS.InitiateCheckout,
|
|
29
|
+
add_payment_info: META_STANDARD_EVENTS.AddPaymentInfo,
|
|
30
|
+
purchase: META_STANDARD_EVENTS.Purchase,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type CartLineLike = {
|
|
34
|
+
id: string;
|
|
35
|
+
variant_id?: string;
|
|
36
|
+
title?: string;
|
|
37
|
+
product_title?: string;
|
|
38
|
+
quantity: number;
|
|
39
|
+
total?: number;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type CartLike = {
|
|
43
|
+
items?: CartLineLike[];
|
|
44
|
+
total?: number;
|
|
45
|
+
currency_code?: string;
|
|
46
|
+
region?: { currency_code?: string };
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function pushToDataLayer(obj: Record<string, unknown>) {
|
|
50
|
+
if (typeof window === "undefined") return;
|
|
51
|
+
const w = window as Window & { dataLayer?: unknown[] };
|
|
52
|
+
w.dataLayer = w.dataLayer || [];
|
|
53
|
+
w.dataLayer.push({ ecommerce: null });
|
|
54
|
+
w.dataLayer.push(obj);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeCurrency(currency?: string) {
|
|
58
|
+
return (currency ?? "INR").toUpperCase();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildMetaFields(payload: EcommercePayload) {
|
|
62
|
+
const items = payload.items ?? [];
|
|
63
|
+
const numItems = items.reduce((sum, i) => sum + (i.quantity ?? 1), 0);
|
|
64
|
+
return {
|
|
65
|
+
currency: normalizeCurrency(payload.currency),
|
|
66
|
+
value: payload.value,
|
|
67
|
+
content_ids: items.map((i) => String(i.item_id)),
|
|
68
|
+
content_type: "product",
|
|
69
|
+
contents: items.map((i) => ({
|
|
70
|
+
id: String(i.item_id),
|
|
71
|
+
quantity: i.quantity ?? 1,
|
|
72
|
+
item_price: i.price ?? 0,
|
|
73
|
+
})),
|
|
74
|
+
num_items: numItems,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function pushEcommerceEvent(
|
|
79
|
+
event: string,
|
|
80
|
+
payload: EcommercePayload,
|
|
81
|
+
extra?: Record<string, unknown>
|
|
82
|
+
) {
|
|
83
|
+
const currency = normalizeCurrency(payload.currency);
|
|
84
|
+
const ecommerce = { ...payload, currency };
|
|
85
|
+
const metaFields = buildMetaFields({ ...payload, currency });
|
|
86
|
+
|
|
87
|
+
pushToDataLayer({
|
|
88
|
+
event,
|
|
89
|
+
ecommerce,
|
|
90
|
+
...metaFields,
|
|
91
|
+
...extra,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const metaEvent = GA4_TO_META_EVENT[event];
|
|
95
|
+
if (metaEvent) {
|
|
96
|
+
if (event === "purchase" && extra?.transaction_id) {
|
|
97
|
+
fireMetaPurchase({
|
|
98
|
+
...payload,
|
|
99
|
+
currency,
|
|
100
|
+
transaction_id: String(extra.transaction_id),
|
|
101
|
+
});
|
|
102
|
+
} else {
|
|
103
|
+
fireMetaEcommerceEvent(metaEvent, { ...payload, currency }, extra);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function trackMetaStandardEvent(
|
|
109
|
+
metaEvent: MetaStandardEventName,
|
|
110
|
+
params?: Record<string, unknown>
|
|
111
|
+
) {
|
|
112
|
+
fireMetaPixelEvent(metaEvent, params ?? {});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function cartToEcommercePayload(cart: CartLike | null): EcommercePayload | null {
|
|
116
|
+
if (!cart?.items?.length) return null;
|
|
117
|
+
|
|
118
|
+
const currency = normalizeCurrency(cart.currency_code ?? cart.region?.currency_code);
|
|
119
|
+
const value = (cart.total ?? 0) / 100;
|
|
120
|
+
const items = cart.items.map((item, index) => {
|
|
121
|
+
const unitTotal = item.total && item.quantity ? item.total / item.quantity : 0;
|
|
122
|
+
return {
|
|
123
|
+
item_id: item.variant_id ?? item.id,
|
|
124
|
+
item_name: item.title ?? item.product_title ?? "",
|
|
125
|
+
price: unitTotal / 100,
|
|
126
|
+
quantity: item.quantity,
|
|
127
|
+
index,
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return { currency, value, items };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function trackAddToCart(payload: EcommercePayload) {
|
|
135
|
+
pushEcommerceEvent("add_to_cart", payload);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function trackRemoveFromCart(payload: EcommercePayload) {
|
|
139
|
+
pushEcommerceEvent("remove_from_cart", payload);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function trackViewCart(payload: EcommercePayload) {
|
|
143
|
+
pushEcommerceEvent("view_cart", payload);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function trackViewItem(payload: EcommercePayload) {
|
|
147
|
+
pushEcommerceEvent("view_item", payload);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function trackViewItemList(payload: {
|
|
151
|
+
list_id?: string;
|
|
152
|
+
list_name?: string;
|
|
153
|
+
items: GA4Item[];
|
|
154
|
+
}) {
|
|
155
|
+
const meta = buildMetaFields({
|
|
156
|
+
currency: "INR",
|
|
157
|
+
value: 0,
|
|
158
|
+
items: payload.items,
|
|
159
|
+
});
|
|
160
|
+
pushToDataLayer({
|
|
161
|
+
event: "view_item_list",
|
|
162
|
+
ecommerce: payload,
|
|
163
|
+
...meta,
|
|
164
|
+
});
|
|
165
|
+
fireMetaEcommerceEvent(META_STANDARD_EVENTS.ViewContent, {
|
|
166
|
+
currency: "INR",
|
|
167
|
+
value: 0,
|
|
168
|
+
items: payload.items,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const INITIATE_CHECKOUT_KEY_PREFIX = "meta_initiate_checkout_";
|
|
173
|
+
|
|
174
|
+
export function markInitiateCheckoutFired(cartId: string) {
|
|
175
|
+
if (typeof window === "undefined") return;
|
|
176
|
+
sessionStorage.setItem(`${INITIATE_CHECKOUT_KEY_PREFIX}${cartId}`, String(Date.now()));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function wasInitiateCheckoutFired(cartId: string, maxAgeMs = 60_000) {
|
|
180
|
+
if (typeof window === "undefined") return false;
|
|
181
|
+
const raw = sessionStorage.getItem(`${INITIATE_CHECKOUT_KEY_PREFIX}${cartId}`);
|
|
182
|
+
if (!raw) return false;
|
|
183
|
+
return Date.now() - Number(raw) < maxAgeMs;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function trackInitiateCheckoutFromCart(payload: EcommercePayload, cartId?: string) {
|
|
187
|
+
trackBeginCheckout(payload);
|
|
188
|
+
if (cartId) markInitiateCheckoutFired(cartId);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function trackBeginCheckout(payload: EcommercePayload) {
|
|
192
|
+
pushEcommerceEvent("begin_checkout", payload);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function trackAddShippingInfo(payload: EcommercePayload) {
|
|
196
|
+
pushEcommerceEvent("add_shipping_info", payload);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function trackAddPaymentInfo(payload: EcommercePayload, extra?: Record<string, unknown>) {
|
|
200
|
+
pushEcommerceEvent("add_payment_info", payload, extra);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function trackPurchase(payload: {
|
|
204
|
+
transaction_id: string;
|
|
205
|
+
currency: string;
|
|
206
|
+
value: number;
|
|
207
|
+
tax?: number;
|
|
208
|
+
shipping?: number;
|
|
209
|
+
items: GA4Item[];
|
|
210
|
+
}) {
|
|
211
|
+
const { transaction_id, ...rest } = payload;
|
|
212
|
+
pushEcommerceEvent("purchase", rest, {
|
|
213
|
+
transaction_id,
|
|
214
|
+
order_id: transaction_id,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function trackAddToWishlist(params: {
|
|
219
|
+
content_ids: string[];
|
|
220
|
+
currency?: string;
|
|
221
|
+
value?: number;
|
|
222
|
+
}) {
|
|
223
|
+
trackMetaStandardEvent(META_STANDARD_EVENTS.AddToWishlist, {
|
|
224
|
+
content_ids: params.content_ids,
|
|
225
|
+
content_type: "product",
|
|
226
|
+
currency: normalizeCurrency(params.currency),
|
|
227
|
+
value: params.value ?? 0,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function trackCompleteRegistration(params?: { status?: string; method?: string }) {
|
|
232
|
+
trackMetaStandardEvent(META_STANDARD_EVENTS.CompleteRegistration, params);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function trackContact(params?: { form_type?: string }) {
|
|
236
|
+
trackMetaStandardEvent(META_STANDARD_EVENTS.Contact, params);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function trackLead(params?: { form_type?: string }) {
|
|
240
|
+
trackMetaStandardEvent(META_STANDARD_EVENTS.Lead, params);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function trackSearch(searchString: string) {
|
|
244
|
+
trackMetaStandardEvent(META_STANDARD_EVENTS.Search, {
|
|
245
|
+
search_string: searchString,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function trackHeroCtaClick(params: { label?: string; link?: string }) {
|
|
250
|
+
trackMetaStandardEvent(META_STANDARD_EVENTS.ViewContent, {
|
|
251
|
+
content_type: "cta",
|
|
252
|
+
content_name: params.label || "shop_now",
|
|
253
|
+
content_category: "hero_banner",
|
|
254
|
+
...(params.link ? { content_url: params.link } : {}),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function trackCartNavClick(cart: CartLike | null) {
|
|
259
|
+
const payload = cartToEcommercePayload(cart);
|
|
260
|
+
if (payload) trackViewCart(payload);
|
|
261
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { FormSubmittedPayload, StorefrontAnalyticsAdapter } from "../core/types";
|
|
2
|
+
|
|
3
|
+
export function emitFormSubmitted(
|
|
4
|
+
analytics: StorefrontAnalyticsAdapter | undefined,
|
|
5
|
+
payload: FormSubmittedPayload
|
|
6
|
+
): void {
|
|
7
|
+
analytics?.trackFormSubmitted?.(payload);
|
|
8
|
+
analytics?.trackEvent?.("form_submit", payload);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function emitContactFormSubmitted(
|
|
12
|
+
analytics: StorefrontAnalyticsAdapter | undefined,
|
|
13
|
+
params?: { formType?: string; [key: string]: unknown }
|
|
14
|
+
): void {
|
|
15
|
+
emitFormSubmitted(analytics, {
|
|
16
|
+
form_type: params?.formType ?? "contact_us_form",
|
|
17
|
+
...params,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function emitLeadCaptured(
|
|
22
|
+
analytics: StorefrontAnalyticsAdapter | undefined,
|
|
23
|
+
params?: { formType?: string; [key: string]: unknown }
|
|
24
|
+
): void {
|
|
25
|
+
if (!analytics) return;
|
|
26
|
+
analytics.trackEvent?.("Lead", {
|
|
27
|
+
form_type: params?.formType ?? "newsletter_signup",
|
|
28
|
+
...params,
|
|
29
|
+
});
|
|
30
|
+
emitFormSubmitted(analytics, {
|
|
31
|
+
form_type: params?.formType ?? "lead",
|
|
32
|
+
...params,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Meta Pixel standard event names (fbq second argument) */
|
|
2
|
+
export const META_STANDARD_EVENTS = {
|
|
3
|
+
AddPaymentInfo: "AddPaymentInfo",
|
|
4
|
+
AddToCart: "AddToCart",
|
|
5
|
+
AddToWishlist: "AddToWishlist",
|
|
6
|
+
CompleteRegistration: "CompleteRegistration",
|
|
7
|
+
Contact: "Contact",
|
|
8
|
+
InitiateCheckout: "InitiateCheckout",
|
|
9
|
+
Lead: "Lead",
|
|
10
|
+
Purchase: "Purchase",
|
|
11
|
+
Search: "Search",
|
|
12
|
+
ViewContent: "ViewContent",
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
export type MetaStandardEventName =
|
|
16
|
+
(typeof META_STANDARD_EVENTS)[keyof typeof META_STANDARD_EVENTS];
|
|
17
|
+
|
|
18
|
+
export type GA4Item = {
|
|
19
|
+
item_id: string;
|
|
20
|
+
item_name: string;
|
|
21
|
+
price?: number;
|
|
22
|
+
quantity?: number;
|
|
23
|
+
index?: number;
|
|
24
|
+
item_variant?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type EcommercePayload = {
|
|
28
|
+
currency: string;
|
|
29
|
+
value: number;
|
|
30
|
+
items: GA4Item[];
|
|
31
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ReviewSubmittedPayload, StorefrontAnalyticsAdapter } from "../core/types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Domain helper: call after a review is successfully saved.
|
|
5
|
+
*/
|
|
6
|
+
export function emitReviewSubmitted(
|
|
7
|
+
analytics: StorefrontAnalyticsAdapter | undefined,
|
|
8
|
+
payload: Omit<ReviewSubmittedPayload, "isUpdate"> & { isUpdate?: boolean }
|
|
9
|
+
): void {
|
|
10
|
+
if (!analytics) return;
|
|
11
|
+
|
|
12
|
+
const full: ReviewSubmittedPayload = {
|
|
13
|
+
productId: payload.productId,
|
|
14
|
+
productName: payload.productName,
|
|
15
|
+
rating: payload.rating,
|
|
16
|
+
reviewTitle: payload.reviewTitle,
|
|
17
|
+
isUpdate: payload.isUpdate ?? false,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
if (full.isUpdate && analytics.trackReviewUpdated) {
|
|
21
|
+
analytics.trackReviewUpdated(full);
|
|
22
|
+
} else {
|
|
23
|
+
analytics.trackReviewSubmitted(full);
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
AnalyticsEventMap,
|
|
3
|
+
FormSubmittedPayload,
|
|
4
|
+
ProductRef,
|
|
5
|
+
ReviewSubmittedPayload,
|
|
6
|
+
StorefrontAnalyticsAdapter,
|
|
7
|
+
} from "./core/types";
|
|
8
|
+
|
|
9
|
+
export { noopAnalyticsAdapter } from "./core/noop-adapter";
|
|
10
|
+
export { composeAnalyticsAdapters } from "./core/compose";
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
createGtmAnalyticsAdapter,
|
|
14
|
+
defaultStorefrontAnalytics,
|
|
15
|
+
pushToDataLayer,
|
|
16
|
+
trackGtmEvent,
|
|
17
|
+
trackEvent,
|
|
18
|
+
initGTM,
|
|
19
|
+
type GtmAnalyticsAdapterOptions,
|
|
20
|
+
} from "./providers/gtm";
|
|
21
|
+
|
|
22
|
+
export { emitReviewSubmitted } from "./events/reviews";
|
|
23
|
+
export {
|
|
24
|
+
emitContactFormSubmitted,
|
|
25
|
+
emitFormSubmitted,
|
|
26
|
+
emitLeadCaptured,
|
|
27
|
+
} from "./events/forms";
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
META_STANDARD_EVENTS,
|
|
31
|
+
type EcommercePayload,
|
|
32
|
+
type GA4Item,
|
|
33
|
+
type MetaStandardEventName,
|
|
34
|
+
} from "./events/meta-events";
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
META_PIXEL_DATALAYER_EVENT,
|
|
38
|
+
fireMetaPixelEvent,
|
|
39
|
+
fireMetaEcommerceEvent,
|
|
40
|
+
fireMetaPurchase,
|
|
41
|
+
} from "./providers/meta-pixel";
|
|
42
|
+
|
|
43
|
+
export {
|
|
44
|
+
META_EXPLICIT_KEY,
|
|
45
|
+
disableMetaPixelAutoConfig,
|
|
46
|
+
installMetaPixelGuard,
|
|
47
|
+
} from "./providers/meta-pixel-guard";
|
|
48
|
+
|
|
49
|
+
export { POST as metaCapiEventHandler } from "./api/meta-event";
|
|
50
|
+
export type { MetaCapiEventBody } from "./api/meta-event";
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
cartToEcommercePayload,
|
|
54
|
+
markInitiateCheckoutFired,
|
|
55
|
+
trackAddPaymentInfo,
|
|
56
|
+
trackAddShippingInfo,
|
|
57
|
+
trackAddToCart,
|
|
58
|
+
trackAddToWishlist,
|
|
59
|
+
trackBeginCheckout,
|
|
60
|
+
trackCartNavClick,
|
|
61
|
+
trackCompleteRegistration,
|
|
62
|
+
trackContact,
|
|
63
|
+
trackHeroCtaClick,
|
|
64
|
+
trackInitiateCheckoutFromCart,
|
|
65
|
+
trackLead,
|
|
66
|
+
trackMetaStandardEvent,
|
|
67
|
+
trackPurchase,
|
|
68
|
+
trackRemoveFromCart,
|
|
69
|
+
trackSearch,
|
|
70
|
+
trackViewCart,
|
|
71
|
+
trackViewItem,
|
|
72
|
+
trackViewItemList,
|
|
73
|
+
wasInitiateCheckoutFired,
|
|
74
|
+
} from "./ecommerce/ga4-ecommerce";
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FormSubmittedPayload,
|
|
3
|
+
ReviewSubmittedPayload,
|
|
4
|
+
StorefrontAnalyticsAdapter,
|
|
5
|
+
} from "../core/types";
|
|
6
|
+
|
|
7
|
+
declare global {
|
|
8
|
+
interface Window {
|
|
9
|
+
dataLayer?: Record<string, unknown>[];
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function pushToDataLayer(data: Record<string, unknown>): void {
|
|
14
|
+
if (typeof window === "undefined") return;
|
|
15
|
+
window.dataLayer = window.dataLayer || [];
|
|
16
|
+
window.dataLayer.push(data);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function trackGtmEvent(eventName: string, params?: Record<string, unknown>): void {
|
|
20
|
+
pushToDataLayer({
|
|
21
|
+
event: eventName,
|
|
22
|
+
...params,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface GtmAnalyticsAdapterOptions {
|
|
27
|
+
/** Also push legacy `form_submit` events (for existing GTM triggers) */
|
|
28
|
+
legacyFormSubmit?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Default GTM / dataLayer adapter for Medusa storefronts.
|
|
33
|
+
*/
|
|
34
|
+
export function createGtmAnalyticsAdapter(
|
|
35
|
+
options: GtmAnalyticsAdapterOptions = {}
|
|
36
|
+
): StorefrontAnalyticsAdapter {
|
|
37
|
+
const { legacyFormSubmit = true } = options;
|
|
38
|
+
|
|
39
|
+
const pushReview = (payload: ReviewSubmittedPayload, eventName: "review_submitted" | "review_updated") => {
|
|
40
|
+
trackGtmEvent(eventName, {
|
|
41
|
+
product_id: payload.productId,
|
|
42
|
+
product_name: payload.productName,
|
|
43
|
+
rating: payload.rating,
|
|
44
|
+
review_title: payload.reviewTitle,
|
|
45
|
+
is_update: payload.isUpdate,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (legacyFormSubmit) {
|
|
49
|
+
trackGtmEvent("form_submit", {
|
|
50
|
+
form_type:
|
|
51
|
+
eventName === "review_updated"
|
|
52
|
+
? "product_review_update"
|
|
53
|
+
: "product_review_submit",
|
|
54
|
+
product_id: payload.productId,
|
|
55
|
+
product_name: payload.productName,
|
|
56
|
+
rating: payload.rating,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
trackReviewSubmitted(payload) {
|
|
63
|
+
pushReview({ ...payload, isUpdate: false }, "review_submitted");
|
|
64
|
+
},
|
|
65
|
+
trackReviewUpdated(payload) {
|
|
66
|
+
pushReview({ ...payload, isUpdate: true }, "review_updated");
|
|
67
|
+
},
|
|
68
|
+
trackFormSubmitted(payload: FormSubmittedPayload) {
|
|
69
|
+
trackGtmEvent("form_submit", payload);
|
|
70
|
+
},
|
|
71
|
+
trackEvent: trackGtmEvent,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Default GTM adapter (legacy form_submit + review events). */
|
|
76
|
+
export const defaultStorefrontAnalytics = createGtmAnalyticsAdapter({
|
|
77
|
+
legacyFormSubmit: true,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
/** @deprecated Use createGtmAnalyticsAdapter */
|
|
81
|
+
export const initGTM = (id: string) => {
|
|
82
|
+
if (typeof window === "undefined") return;
|
|
83
|
+
pushToDataLayer({
|
|
84
|
+
"gtm.start": new Date().getTime(),
|
|
85
|
+
event: "gtm.js",
|
|
86
|
+
});
|
|
87
|
+
const firstScript = document.getElementsByTagName("script")[0];
|
|
88
|
+
const script = document.createElement("script") as HTMLScriptElement;
|
|
89
|
+
script.async = true;
|
|
90
|
+
script.src = `https://www.googletagmanager.com/gtm.js?id=${id}`;
|
|
91
|
+
if (firstScript?.parentNode) {
|
|
92
|
+
firstScript.parentNode.insertBefore(script, firstScript);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** @deprecated Use trackGtmEvent */
|
|
97
|
+
export const trackEvent = trackGtmEvent;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blocks Meta automatic mis-classification (e.g. Subscribe on non-newsletter clicks).
|
|
3
|
+
* Only events fired via fireMetaPixelEvent() include __cm_explicit and pass through.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const META_EXPLICIT_KEY = "__cm_explicit";
|
|
7
|
+
|
|
8
|
+
/** Disable Meta pixel automatic event configuration (button scraping, etc.) */
|
|
9
|
+
export function disableMetaPixelAutoConfig() {
|
|
10
|
+
if (typeof window === "undefined") return false;
|
|
11
|
+
const fbq = (window as Window & { fbq?: (...args: unknown[]) => void }).fbq;
|
|
12
|
+
if (typeof fbq !== "function") return false;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
fbq("set", "autoConfig", false);
|
|
16
|
+
} catch {
|
|
17
|
+
// Pixel may not be fully initialized yet
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function shouldBlockMetaTrack(eventName: unknown, params: unknown): boolean {
|
|
23
|
+
if (typeof eventName !== "string") return false;
|
|
24
|
+
if (eventName === "Subscribe") {
|
|
25
|
+
const p = params as Record<string, unknown> | undefined;
|
|
26
|
+
return !p?.[META_EXPLICIT_KEY];
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function patchFbq() {
|
|
32
|
+
if (typeof window === "undefined") return false;
|
|
33
|
+
const w = window as Window & {
|
|
34
|
+
fbq?: (...args: unknown[]) => void;
|
|
35
|
+
_fbq?: (...args: unknown[]) => void;
|
|
36
|
+
__cmFbqPatched?: boolean;
|
|
37
|
+
};
|
|
38
|
+
const fbq = w.fbq;
|
|
39
|
+
if (typeof fbq !== "function" || w.__cmFbqPatched) return true;
|
|
40
|
+
|
|
41
|
+
const original = fbq;
|
|
42
|
+
const patched = function (this: unknown, ...args: unknown[]) {
|
|
43
|
+
if (args[0] === "track" && shouldBlockMetaTrack(args[1], args[2])) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
return original.apply(this, args);
|
|
47
|
+
};
|
|
48
|
+
Object.assign(patched, original);
|
|
49
|
+
w.fbq = patched;
|
|
50
|
+
if (w._fbq) w._fbq = patched;
|
|
51
|
+
w.__cmFbqPatched = true;
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function patchDataLayer() {
|
|
56
|
+
if (typeof window === "undefined") return false;
|
|
57
|
+
const w = window as Window & {
|
|
58
|
+
dataLayer?: unknown[];
|
|
59
|
+
__cmDataLayerPatched?: boolean;
|
|
60
|
+
};
|
|
61
|
+
if (!w.dataLayer || w.__cmDataLayerPatched) return true;
|
|
62
|
+
|
|
63
|
+
const originalPush = w.dataLayer.push.bind(w.dataLayer);
|
|
64
|
+
w.dataLayer.push = function (...items: unknown[]) {
|
|
65
|
+
const filtered = items.filter((item) => {
|
|
66
|
+
if (!item || typeof item !== "object") return true;
|
|
67
|
+
const obj = item as Record<string, unknown>;
|
|
68
|
+
const metaEvent = obj.meta_event;
|
|
69
|
+
if (metaEvent === "Subscribe" && !obj[META_EXPLICIT_KEY]) return false;
|
|
70
|
+
if (obj.event === "subscribe" && !obj[META_EXPLICIT_KEY]) return false;
|
|
71
|
+
return true;
|
|
72
|
+
});
|
|
73
|
+
return originalPush(...filtered);
|
|
74
|
+
};
|
|
75
|
+
w.__cmDataLayerPatched = true;
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Call once on app load; retries until GTM/pixel has initialized fbq */
|
|
80
|
+
export function installMetaPixelGuard() {
|
|
81
|
+
if (typeof window === "undefined") return;
|
|
82
|
+
|
|
83
|
+
patchDataLayer();
|
|
84
|
+
|
|
85
|
+
let attempts = 0;
|
|
86
|
+
const maxAttempts = 60;
|
|
87
|
+
const tick = () => {
|
|
88
|
+
patchDataLayer();
|
|
89
|
+
const fbqReady = patchFbq();
|
|
90
|
+
if (fbqReady) disableMetaPixelAutoConfig();
|
|
91
|
+
if (!fbqReady && ++attempts < maxAttempts) {
|
|
92
|
+
setTimeout(tick, 250);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
tick();
|
|
96
|
+
}
|