@mitchdesigns/tracking 1.2.4
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/package.json +29 -0
- package/src/components/GTMScript.tsx +61 -0
- package/src/components/MetaPixel.tsx +96 -0
- package/src/hooks/useGTMTracking.ts +199 -0
- package/src/hooks/useMetaTracking.ts +225 -0
- package/src/index.ts +11 -0
- package/src/lib/capi.ts +76 -0
- package/src/lib/capiHandler.ts +92 -0
- package/src/lib/gtm.ts +78 -0
- package/src/lib/hashing.ts +35 -0
- package/src/lib/pixel.ts +115 -0
- package/src/lib/types.ts +63 -0
- package/src/server.ts +1 -0
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mitchdesigns/tracking",
|
|
3
|
+
"version": "1.2.4",
|
|
4
|
+
"description": "Tracking package for Next.js — Meta Pixel, Meta CAPI, and Google Tag Manager hooks and components",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"files": [
|
|
7
|
+
"src"
|
|
8
|
+
],
|
|
9
|
+
"main": "./src/index.ts",
|
|
10
|
+
"types": "./src/index.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./src/index.ts",
|
|
14
|
+
"default": "./src/index.ts"
|
|
15
|
+
},
|
|
16
|
+
"./server": {
|
|
17
|
+
"types": "./src/server.ts",
|
|
18
|
+
"default": "./src/server.ts"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"registry": "https://registry.npmjs.org",
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"next": ">=14.0.0",
|
|
27
|
+
"react": ">=18.0.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
import { usePathname, useSearchParams } from "next/navigation";
|
|
5
|
+
import Script from "next/script";
|
|
6
|
+
import { pushToDataLayer } from "../lib/gtm";
|
|
7
|
+
|
|
8
|
+
export default function GTMScript() {
|
|
9
|
+
const pathname = usePathname();
|
|
10
|
+
const searchParams = useSearchParams();
|
|
11
|
+
const gtmId = process.env.NEXT_PUBLIC_GTM_ID;
|
|
12
|
+
const initialized = useRef(false);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!gtmId || !initialized.current) return;
|
|
16
|
+
|
|
17
|
+
// SPA navigation — push page_view on route change
|
|
18
|
+
pushToDataLayer({
|
|
19
|
+
event: "page_view",
|
|
20
|
+
page_path: pathname + (searchParams.toString() ? `?${searchParams.toString()}` : ""),
|
|
21
|
+
page_title: document.title,
|
|
22
|
+
});
|
|
23
|
+
}, [pathname, searchParams, gtmId]);
|
|
24
|
+
|
|
25
|
+
if (!gtmId) return null;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<>
|
|
29
|
+
{/* Step 1: Initialize dataLayer and load GTM */}
|
|
30
|
+
<Script
|
|
31
|
+
id="gtm-init"
|
|
32
|
+
strategy="afterInteractive"
|
|
33
|
+
dangerouslySetInnerHTML={{
|
|
34
|
+
__html: `
|
|
35
|
+
window.dataLayer = window.dataLayer || [];
|
|
36
|
+
window.dataLayer.push({'gtm.start': new Date().getTime(), event: 'gtm.js'});
|
|
37
|
+
`,
|
|
38
|
+
}}
|
|
39
|
+
/>
|
|
40
|
+
<Script
|
|
41
|
+
id="gtm-sdk"
|
|
42
|
+
src={`https://www.googletagmanager.com/gtm.js?id=${gtmId}`}
|
|
43
|
+
strategy="afterInteractive"
|
|
44
|
+
onLoad={() => {
|
|
45
|
+
if (!initialized.current) {
|
|
46
|
+
initialized.current = true;
|
|
47
|
+
}
|
|
48
|
+
}}
|
|
49
|
+
/>
|
|
50
|
+
{/* Noscript fallback — place <GTMScript /> as first child of <body> in layout.tsx */}
|
|
51
|
+
<noscript>
|
|
52
|
+
<iframe
|
|
53
|
+
src={`https://www.googletagmanager.com/ns.html?id=${gtmId}`}
|
|
54
|
+
height="0"
|
|
55
|
+
width="0"
|
|
56
|
+
style={{ display: "none", visibility: "hidden" }}
|
|
57
|
+
/>
|
|
58
|
+
</noscript>
|
|
59
|
+
</>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
import { usePathname, useSearchParams } from "next/navigation";
|
|
5
|
+
import Script from "next/script";
|
|
6
|
+
import Image from "next/image";
|
|
7
|
+
import { useMetaTracking } from "../hooks/useMetaTracking";
|
|
8
|
+
|
|
9
|
+
export default function MetaPixel() {
|
|
10
|
+
const pathname = usePathname();
|
|
11
|
+
const searchParams = useSearchParams();
|
|
12
|
+
const pixelId = process.env.NEXT_PUBLIC_META_PIXEL_ID;
|
|
13
|
+
const { trackPageView } = useMetaTracking();
|
|
14
|
+
const initialized = useRef(false);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!pixelId) return;
|
|
18
|
+
|
|
19
|
+
if (typeof window !== "undefined") {
|
|
20
|
+
const expires = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toUTCString();
|
|
21
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
22
|
+
const fbclid = urlParams.get("fbclid");
|
|
23
|
+
|
|
24
|
+
if (fbclid && !document.cookie.includes("_fbc=")) {
|
|
25
|
+
const fbcValue = `fb.1.${Date.now()}.${fbclid}`;
|
|
26
|
+
document.cookie = `_fbc=${fbcValue}; expires=${expires}; path=/; SameSite=Lax`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!document.cookie.includes("_fbp=")) {
|
|
30
|
+
const fbpValue = `fb.1.${Date.now()}.${Math.floor(Math.random() * 1000000000)}`;
|
|
31
|
+
document.cookie = `_fbp=${fbpValue}; expires=${expires}; path=/; SameSite=Lax`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!document.cookie.includes("_fb_ip=")) {
|
|
35
|
+
fetch("https://api64.ipify.org?format=json")
|
|
36
|
+
.then((res) => res.json())
|
|
37
|
+
.then((data) => {
|
|
38
|
+
if (data.ip) {
|
|
39
|
+
const ipExpires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toUTCString();
|
|
40
|
+
document.cookie = `_fb_ip=${data.ip}; expires=${ipExpires}; path=/; SameSite=Lax`;
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
.catch(() => {});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// SPA navigation — fbevents.js is already loaded by now
|
|
48
|
+
if (initialized.current) {
|
|
49
|
+
trackPageView();
|
|
50
|
+
}
|
|
51
|
+
}, [pathname, searchParams, pixelId]);
|
|
52
|
+
|
|
53
|
+
if (!pixelId) return null;
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<>
|
|
57
|
+
{/* Step 1: define fbq stub + init inline so calls are queued before fbevents.js loads */}
|
|
58
|
+
<Script
|
|
59
|
+
id="fb-pixel-init"
|
|
60
|
+
strategy="afterInteractive"
|
|
61
|
+
dangerouslySetInnerHTML={{
|
|
62
|
+
__html: `
|
|
63
|
+
!function(f,b,e,v,n,t,s)
|
|
64
|
+
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
|
|
65
|
+
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
|
|
66
|
+
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
|
|
67
|
+
n.queue=[];}(window,document,'script',
|
|
68
|
+
'https://connect.facebook.net/en_US/fbevents.js');
|
|
69
|
+
fbq('init', '${pixelId}');
|
|
70
|
+
`,
|
|
71
|
+
}}
|
|
72
|
+
/>
|
|
73
|
+
{/* Step 2: load fbevents.js — onLoad fires when the SDK is ready */}
|
|
74
|
+
<Script
|
|
75
|
+
id="fb-pixel-sdk"
|
|
76
|
+
src="https://connect.facebook.net/en_US/fbevents.js"
|
|
77
|
+
strategy="afterInteractive"
|
|
78
|
+
onLoad={() => {
|
|
79
|
+
if (!initialized.current) {
|
|
80
|
+
initialized.current = true;
|
|
81
|
+
trackPageView(); // pixel + CAPI share the same event_id → deduplication works
|
|
82
|
+
}
|
|
83
|
+
}}
|
|
84
|
+
/>
|
|
85
|
+
<noscript>
|
|
86
|
+
<Image
|
|
87
|
+
height={1}
|
|
88
|
+
width={1}
|
|
89
|
+
style={{ display: "none" }}
|
|
90
|
+
src={`https://www.facebook.com/tr?id=${pixelId}&ev=PageView&noscript=1`}
|
|
91
|
+
alt="pixel-no-script"
|
|
92
|
+
/>
|
|
93
|
+
</noscript>
|
|
94
|
+
</>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useMemo } from "react";
|
|
4
|
+
import {
|
|
5
|
+
pushToDataLayer,
|
|
6
|
+
pushEcommerceEvent,
|
|
7
|
+
contentsToGA4Items,
|
|
8
|
+
buildGTMUserData,
|
|
9
|
+
} from "../lib/gtm";
|
|
10
|
+
|
|
11
|
+
type RichContent = {
|
|
12
|
+
id: string;
|
|
13
|
+
quantity: number;
|
|
14
|
+
item_price: number;
|
|
15
|
+
item_name?: string;
|
|
16
|
+
item_brand?: string;
|
|
17
|
+
item_category?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const useGTMTracking = () => {
|
|
21
|
+
const trackPageView = useCallback((pagePath?: string, pageTitle?: string) => {
|
|
22
|
+
pushToDataLayer({
|
|
23
|
+
event: "page_view",
|
|
24
|
+
page_path:
|
|
25
|
+
pagePath ??
|
|
26
|
+
(typeof window !== "undefined"
|
|
27
|
+
? window.location.pathname + window.location.search
|
|
28
|
+
: ""),
|
|
29
|
+
page_title:
|
|
30
|
+
pageTitle ?? (typeof document !== "undefined" ? document.title : ""),
|
|
31
|
+
});
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
const trackViewContent = useCallback(
|
|
35
|
+
(data: {
|
|
36
|
+
content_ids: string[];
|
|
37
|
+
content_name: string;
|
|
38
|
+
content_category?: string;
|
|
39
|
+
value: number;
|
|
40
|
+
currency?: string;
|
|
41
|
+
}) => {
|
|
42
|
+
pushEcommerceEvent("view_item", {
|
|
43
|
+
currency: data.currency || "EGP",
|
|
44
|
+
value: data.value,
|
|
45
|
+
items: data.content_ids.map((id) => ({
|
|
46
|
+
item_id: id,
|
|
47
|
+
item_name: data.content_name,
|
|
48
|
+
...(data.content_category && { item_category: data.content_category }),
|
|
49
|
+
price: data.value,
|
|
50
|
+
})),
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
[],
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const trackViewCategory = useCallback(
|
|
57
|
+
(data: {
|
|
58
|
+
content_category: string;
|
|
59
|
+
content_ids: string[];
|
|
60
|
+
currency?: string;
|
|
61
|
+
}) => {
|
|
62
|
+
pushEcommerceEvent("view_item_list", {
|
|
63
|
+
item_list_name: data.content_category,
|
|
64
|
+
items: data.content_ids.map((id) => ({
|
|
65
|
+
item_id: id,
|
|
66
|
+
item_category: data.content_category,
|
|
67
|
+
item_list_name: data.content_category,
|
|
68
|
+
})),
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
[],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const trackAddToCart = useCallback(
|
|
75
|
+
(data: {
|
|
76
|
+
content_ids: string[];
|
|
77
|
+
contents: RichContent[];
|
|
78
|
+
content_name?: string;
|
|
79
|
+
value: number;
|
|
80
|
+
currency?: string;
|
|
81
|
+
}) => {
|
|
82
|
+
pushEcommerceEvent("add_to_cart", {
|
|
83
|
+
currency: data.currency || "EGP",
|
|
84
|
+
value: data.value,
|
|
85
|
+
items: contentsToGA4Items(data.contents, data.content_name),
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
[],
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const trackViewCart = useCallback(
|
|
92
|
+
(data: {
|
|
93
|
+
contents: RichContent[];
|
|
94
|
+
value: number;
|
|
95
|
+
currency?: string;
|
|
96
|
+
}) => {
|
|
97
|
+
pushEcommerceEvent("view_cart", {
|
|
98
|
+
currency: data.currency || "EGP",
|
|
99
|
+
value: data.value,
|
|
100
|
+
items: contentsToGA4Items(data.contents),
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
[],
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const trackInitiateCheckout = useCallback(
|
|
107
|
+
(data: {
|
|
108
|
+
content_ids: string[];
|
|
109
|
+
contents: RichContent[];
|
|
110
|
+
value: number;
|
|
111
|
+
currency?: string;
|
|
112
|
+
}) => {
|
|
113
|
+
pushEcommerceEvent("begin_checkout", {
|
|
114
|
+
currency: data.currency || "EGP",
|
|
115
|
+
value: data.value,
|
|
116
|
+
items: contentsToGA4Items(data.contents),
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
[],
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const trackAddPaymentInfo = useCallback(
|
|
123
|
+
(data: {
|
|
124
|
+
content_ids: string[];
|
|
125
|
+
contents: RichContent[];
|
|
126
|
+
value: number;
|
|
127
|
+
currency?: string;
|
|
128
|
+
}) => {
|
|
129
|
+
pushEcommerceEvent("add_payment_info", {
|
|
130
|
+
currency: data.currency || "EGP",
|
|
131
|
+
value: data.value,
|
|
132
|
+
items: contentsToGA4Items(data.contents),
|
|
133
|
+
});
|
|
134
|
+
},
|
|
135
|
+
[],
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const trackPurchase = useCallback(
|
|
139
|
+
(data: {
|
|
140
|
+
orderId: string;
|
|
141
|
+
value: number;
|
|
142
|
+
currency?: string;
|
|
143
|
+
contents: RichContent[];
|
|
144
|
+
content_name?: string;
|
|
145
|
+
user?: any;
|
|
146
|
+
}) => {
|
|
147
|
+
const userData = buildGTMUserData(data.user);
|
|
148
|
+
pushEcommerceEvent(
|
|
149
|
+
"purchase",
|
|
150
|
+
{
|
|
151
|
+
transaction_id: data.orderId,
|
|
152
|
+
currency: data.currency || "EGP",
|
|
153
|
+
value: data.value,
|
|
154
|
+
items: contentsToGA4Items(data.contents, data.content_name),
|
|
155
|
+
},
|
|
156
|
+
userData ? { user_data: userData } : undefined,
|
|
157
|
+
);
|
|
158
|
+
},
|
|
159
|
+
[],
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const trackCompleteRegistration = useCallback((method?: string) => {
|
|
163
|
+
pushToDataLayer({
|
|
164
|
+
event: "sign_up",
|
|
165
|
+
method: method ?? "email",
|
|
166
|
+
});
|
|
167
|
+
}, []);
|
|
168
|
+
|
|
169
|
+
const trackContact = useCallback(() => {
|
|
170
|
+
pushToDataLayer({ event: "generate_lead" });
|
|
171
|
+
}, []);
|
|
172
|
+
|
|
173
|
+
return useMemo(
|
|
174
|
+
() => ({
|
|
175
|
+
trackPageView,
|
|
176
|
+
trackViewContent,
|
|
177
|
+
trackViewCategory,
|
|
178
|
+
trackAddToCart,
|
|
179
|
+
trackViewCart,
|
|
180
|
+
trackInitiateCheckout,
|
|
181
|
+
trackAddPaymentInfo,
|
|
182
|
+
trackPurchase,
|
|
183
|
+
trackCompleteRegistration,
|
|
184
|
+
trackContact,
|
|
185
|
+
}),
|
|
186
|
+
[
|
|
187
|
+
trackPageView,
|
|
188
|
+
trackViewContent,
|
|
189
|
+
trackViewCategory,
|
|
190
|
+
trackAddToCart,
|
|
191
|
+
trackViewCart,
|
|
192
|
+
trackInitiateCheckout,
|
|
193
|
+
trackAddPaymentInfo,
|
|
194
|
+
trackPurchase,
|
|
195
|
+
trackCompleteRegistration,
|
|
196
|
+
trackContact,
|
|
197
|
+
],
|
|
198
|
+
);
|
|
199
|
+
};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo, useCallback } from "react";
|
|
4
|
+
import {
|
|
5
|
+
generateEventId,
|
|
6
|
+
getEnhancedUserData,
|
|
7
|
+
trackPageView as pixelTrackPageView,
|
|
8
|
+
trackViewContent as pixelTrackViewContent,
|
|
9
|
+
trackAddToCart as pixelTrackAddToCart,
|
|
10
|
+
trackPurchase as pixelTrackPurchase,
|
|
11
|
+
} from "../lib/pixel";
|
|
12
|
+
import {
|
|
13
|
+
sendCAPIEvent,
|
|
14
|
+
sendPurchaseEvent as capiSendPurchaseEvent,
|
|
15
|
+
} from "../lib/capi";
|
|
16
|
+
|
|
17
|
+
export const useMetaTracking = (user?: any) => {
|
|
18
|
+
const enhancedUserData = useMemo(() => getEnhancedUserData(user), [user]);
|
|
19
|
+
|
|
20
|
+
const trackPageView = useCallback(
|
|
21
|
+
(eventId?: string) => {
|
|
22
|
+
const id = eventId || generateEventId("pgv");
|
|
23
|
+
pixelTrackPageView(id);
|
|
24
|
+
sendCAPIEvent({ eventName: "PageView", eventId: id, userData: enhancedUserData });
|
|
25
|
+
},
|
|
26
|
+
[enhancedUserData],
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const trackViewContent = useCallback(
|
|
30
|
+
(
|
|
31
|
+
data: {
|
|
32
|
+
content_ids: string[];
|
|
33
|
+
content_name: string;
|
|
34
|
+
content_category?: string;
|
|
35
|
+
value: number;
|
|
36
|
+
currency?: string;
|
|
37
|
+
},
|
|
38
|
+
eventId?: string,
|
|
39
|
+
) => {
|
|
40
|
+
const id = eventId || generateEventId("vc");
|
|
41
|
+
const currency = data.currency || "EGP";
|
|
42
|
+
pixelTrackViewContent({ ...data, currency }, id);
|
|
43
|
+
sendCAPIEvent({
|
|
44
|
+
eventName: "ViewContent",
|
|
45
|
+
eventId: id,
|
|
46
|
+
customData: { ...data, currency, content_type: "product", num_items: data.content_ids.length },
|
|
47
|
+
userData: enhancedUserData,
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
[enhancedUserData],
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const trackAddToCart = useCallback(
|
|
54
|
+
(
|
|
55
|
+
data: {
|
|
56
|
+
content_ids: string[];
|
|
57
|
+
contents: Array<{ id: string; quantity: number; item_price: number }>;
|
|
58
|
+
content_name?: string;
|
|
59
|
+
value: number;
|
|
60
|
+
currency?: string;
|
|
61
|
+
},
|
|
62
|
+
eventId?: string,
|
|
63
|
+
) => {
|
|
64
|
+
const id = eventId || generateEventId("atc");
|
|
65
|
+
const currency = data.currency || "EGP";
|
|
66
|
+
pixelTrackAddToCart({ ...data, currency }, id);
|
|
67
|
+
sendCAPIEvent({
|
|
68
|
+
eventName: "AddToCart",
|
|
69
|
+
eventId: id,
|
|
70
|
+
customData: {
|
|
71
|
+
...data,
|
|
72
|
+
currency,
|
|
73
|
+
content_type: "product",
|
|
74
|
+
num_items: data.contents.reduce((acc, item) => acc + item.quantity, 0),
|
|
75
|
+
},
|
|
76
|
+
userData: enhancedUserData,
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
[enhancedUserData],
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const trackPurchase = useCallback(
|
|
83
|
+
(data: {
|
|
84
|
+
orderId: string;
|
|
85
|
+
value: number;
|
|
86
|
+
currency?: string;
|
|
87
|
+
contents: Array<{ id: string; quantity: number; item_price: number }>;
|
|
88
|
+
content_ids: string[];
|
|
89
|
+
numItems: number;
|
|
90
|
+
}) => {
|
|
91
|
+
const currency = data.currency || "EGP";
|
|
92
|
+
// Explicitly map camelCase → snake_case — do NOT spread data directly
|
|
93
|
+
// as it leaks orderId/numItems into pixel custom data, causing validation errors
|
|
94
|
+
pixelTrackPurchase({
|
|
95
|
+
content_ids: data.content_ids,
|
|
96
|
+
contents: data.contents,
|
|
97
|
+
value: data.value,
|
|
98
|
+
currency,
|
|
99
|
+
num_items: data.numItems,
|
|
100
|
+
order_id: data.orderId,
|
|
101
|
+
});
|
|
102
|
+
capiSendPurchaseEvent({ ...data, currency, userData: enhancedUserData });
|
|
103
|
+
},
|
|
104
|
+
[enhancedUserData],
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const trackInitiateCheckout = useCallback(
|
|
108
|
+
(
|
|
109
|
+
data: {
|
|
110
|
+
content_ids: string[];
|
|
111
|
+
contents: Array<{ id: string; quantity: number; item_price: number }>;
|
|
112
|
+
value: number;
|
|
113
|
+
currency?: string;
|
|
114
|
+
},
|
|
115
|
+
eventId?: string,
|
|
116
|
+
) => {
|
|
117
|
+
const id = eventId || generateEventId("ic");
|
|
118
|
+
const currency = data.currency || "EGP";
|
|
119
|
+
const num_items = data.contents.reduce((acc, item) => acc + item.quantity, 0);
|
|
120
|
+
if (typeof window !== "undefined" && window.fbq) {
|
|
121
|
+
window.fbq("track", "InitiateCheckout", { ...data, currency, content_type: "product", num_items }, { eventID: id });
|
|
122
|
+
}
|
|
123
|
+
sendCAPIEvent({
|
|
124
|
+
eventName: "InitiateCheckout",
|
|
125
|
+
eventId: id,
|
|
126
|
+
customData: { ...data, currency, content_type: "product", num_items },
|
|
127
|
+
userData: enhancedUserData,
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
[enhancedUserData],
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const trackViewCategory = useCallback(
|
|
134
|
+
(
|
|
135
|
+
data: { content_category: string; content_ids: string[]; currency?: string },
|
|
136
|
+
eventId?: string,
|
|
137
|
+
) => {
|
|
138
|
+
const id = eventId || generateEventId("ctv");
|
|
139
|
+
const currency = data.currency || "EGP";
|
|
140
|
+
if (typeof window !== "undefined" && window.fbq) {
|
|
141
|
+
window.fbq("trackCustom", "ViewCategory", { ...data }, { eventID: id });
|
|
142
|
+
}
|
|
143
|
+
sendCAPIEvent({
|
|
144
|
+
eventName: "ViewCategory",
|
|
145
|
+
eventId: id,
|
|
146
|
+
customData: { ...data, currency, content_type: "product", num_items: data.content_ids.length },
|
|
147
|
+
userData: enhancedUserData,
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
[enhancedUserData],
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const trackContact = useCallback(
|
|
154
|
+
(eventId?: string) => {
|
|
155
|
+
const id = eventId || generateEventId("contact");
|
|
156
|
+
if (typeof window !== "undefined" && window.fbq) {
|
|
157
|
+
window.fbq("track", "Contact", {}, { eventID: id });
|
|
158
|
+
}
|
|
159
|
+
sendCAPIEvent({ eventName: "Contact", eventId: id, userData: enhancedUserData });
|
|
160
|
+
},
|
|
161
|
+
[enhancedUserData],
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const trackCompleteRegistration = useCallback(
|
|
165
|
+
(eventId?: string) => {
|
|
166
|
+
const id = eventId || generateEventId("reg");
|
|
167
|
+
if (typeof window !== "undefined" && window.fbq) {
|
|
168
|
+
window.fbq("track", "CompleteRegistration", {}, { eventID: id });
|
|
169
|
+
}
|
|
170
|
+
sendCAPIEvent({ eventName: "CompleteRegistration", eventId: id, userData: enhancedUserData });
|
|
171
|
+
},
|
|
172
|
+
[enhancedUserData],
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const trackAddPaymentInfo = useCallback(
|
|
176
|
+
(
|
|
177
|
+
data: {
|
|
178
|
+
content_ids: string[];
|
|
179
|
+
contents: Array<{ id: string; quantity: number; item_price: number }>;
|
|
180
|
+
value: number;
|
|
181
|
+
currency?: string;
|
|
182
|
+
},
|
|
183
|
+
eventId?: string,
|
|
184
|
+
) => {
|
|
185
|
+
const id = eventId || generateEventId("api");
|
|
186
|
+
const currency = data.currency || "EGP";
|
|
187
|
+
const num_items = data.contents.reduce((acc, item) => acc + item.quantity, 0);
|
|
188
|
+
if (typeof window !== "undefined" && window.fbq) {
|
|
189
|
+
window.fbq("track", "AddPaymentInfo", { ...data, currency, content_type: "product", num_items }, { eventID: id });
|
|
190
|
+
}
|
|
191
|
+
sendCAPIEvent({
|
|
192
|
+
eventName: "AddPaymentInfo",
|
|
193
|
+
eventId: id,
|
|
194
|
+
customData: { ...data, currency, content_type: "product", num_items },
|
|
195
|
+
userData: enhancedUserData,
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
[enhancedUserData],
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
return useMemo(
|
|
202
|
+
() => ({
|
|
203
|
+
trackPageView,
|
|
204
|
+
trackViewContent,
|
|
205
|
+
trackAddToCart,
|
|
206
|
+
trackInitiateCheckout,
|
|
207
|
+
trackAddPaymentInfo,
|
|
208
|
+
trackPurchase,
|
|
209
|
+
trackViewCategory,
|
|
210
|
+
trackContact,
|
|
211
|
+
trackCompleteRegistration,
|
|
212
|
+
}),
|
|
213
|
+
[
|
|
214
|
+
trackPageView,
|
|
215
|
+
trackViewContent,
|
|
216
|
+
trackAddToCart,
|
|
217
|
+
trackInitiateCheckout,
|
|
218
|
+
trackAddPaymentInfo,
|
|
219
|
+
trackPurchase,
|
|
220
|
+
trackViewCategory,
|
|
221
|
+
trackContact,
|
|
222
|
+
trackCompleteRegistration,
|
|
223
|
+
],
|
|
224
|
+
);
|
|
225
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
export { default as MetaPixel } from "./components/MetaPixel";
|
|
4
|
+
export { useMetaTracking } from "./hooks/useMetaTracking";
|
|
5
|
+
export * from "./lib/pixel";
|
|
6
|
+
export * from "./lib/capi";
|
|
7
|
+
export * from "./lib/types";
|
|
8
|
+
|
|
9
|
+
export { default as GTMScript } from "./components/GTMScript";
|
|
10
|
+
export { useGTMTracking } from "./hooks/useGTMTracking";
|
|
11
|
+
export { pushToDataLayer, pushEcommerceEvent, contentsToGA4Items, buildGTMUserData } from "./lib/gtm";
|
package/src/lib/capi.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { CAPIUserData } from "./types";
|
|
2
|
+
|
|
3
|
+
export async function sendCAPIEvent(params: {
|
|
4
|
+
eventName: string;
|
|
5
|
+
eventTime?: number;
|
|
6
|
+
eventId?: string;
|
|
7
|
+
customData?: Record<string, unknown>;
|
|
8
|
+
userData: CAPIUserData;
|
|
9
|
+
testEventCode?: string;
|
|
10
|
+
}): Promise<void> {
|
|
11
|
+
const { eventName, eventTime, eventId, customData, userData, testEventCode } = params;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const payload = {
|
|
15
|
+
event_name: eventName,
|
|
16
|
+
event_time: eventTime || Math.floor(Date.now() / 1000),
|
|
17
|
+
event_id: eventId,
|
|
18
|
+
event_source_url:
|
|
19
|
+
typeof window !== "undefined" ? window.location.href : undefined,
|
|
20
|
+
action_source: "website",
|
|
21
|
+
user_data: userData,
|
|
22
|
+
custom_data: customData,
|
|
23
|
+
test_event_code:
|
|
24
|
+
testEventCode || process.env.NEXT_PUBLIC_META_TEST_EVENT_CODE,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const response = await fetch("/api/meta/capi", {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: { "Content-Type": "application/json" },
|
|
30
|
+
body: JSON.stringify(payload),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
const error = await response.json();
|
|
35
|
+
console.error("Meta CAPI failed:", error);
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error("Network error sending Meta CAPI event:", error);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function sendPurchaseEvent(params: {
|
|
43
|
+
orderId: string;
|
|
44
|
+
value: number;
|
|
45
|
+
currency: string;
|
|
46
|
+
contents: Array<{ id: string; quantity: number; item_price: number }>;
|
|
47
|
+
content_type?: string;
|
|
48
|
+
content_ids?: string[];
|
|
49
|
+
numItems: number;
|
|
50
|
+
userData: CAPIUserData;
|
|
51
|
+
}): Promise<void> {
|
|
52
|
+
const {
|
|
53
|
+
orderId,
|
|
54
|
+
value,
|
|
55
|
+
currency,
|
|
56
|
+
contents,
|
|
57
|
+
content_type,
|
|
58
|
+
content_ids,
|
|
59
|
+
numItems,
|
|
60
|
+
userData,
|
|
61
|
+
} = params;
|
|
62
|
+
|
|
63
|
+
await sendCAPIEvent({
|
|
64
|
+
eventName: "Purchase",
|
|
65
|
+
eventId: orderId,
|
|
66
|
+
customData: {
|
|
67
|
+
value,
|
|
68
|
+
currency,
|
|
69
|
+
contents,
|
|
70
|
+
content_type: content_type || "product",
|
|
71
|
+
content_ids: content_ids || contents.map((c) => c.id),
|
|
72
|
+
num_items: numItems,
|
|
73
|
+
},
|
|
74
|
+
userData,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { hashUserData } from "./hashing";
|
|
3
|
+
import type { CAPIEvent } from "./types";
|
|
4
|
+
|
|
5
|
+
export const runtime = "edge";
|
|
6
|
+
|
|
7
|
+
export async function POST(req: NextRequest) {
|
|
8
|
+
try {
|
|
9
|
+
const body = await req.json();
|
|
10
|
+
const {
|
|
11
|
+
event_name,
|
|
12
|
+
event_time,
|
|
13
|
+
event_id,
|
|
14
|
+
event_source_url,
|
|
15
|
+
action_source = "website",
|
|
16
|
+
user_data,
|
|
17
|
+
custom_data,
|
|
18
|
+
test_event_code,
|
|
19
|
+
} = body;
|
|
20
|
+
|
|
21
|
+
const accessToken = process.env.META_ACCESS_TOKEN;
|
|
22
|
+
const pixelId = process.env.NEXT_PUBLIC_META_PIXEL_ID;
|
|
23
|
+
|
|
24
|
+
if (!accessToken || !pixelId) {
|
|
25
|
+
return NextResponse.json({ error: "Missing configuration" }, { status: 500 });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const fbc = req.cookies.get("_fbc")?.value;
|
|
29
|
+
const fbp = req.cookies.get("_fbp")?.value;
|
|
30
|
+
const cookieIp = req.cookies.get("_fb_ip")?.value;
|
|
31
|
+
|
|
32
|
+
const clientIp =
|
|
33
|
+
cookieIp ||
|
|
34
|
+
req.headers.get("x-forwarded-for")?.split(",")[0] ||
|
|
35
|
+
req.headers.get("x-real-ip") ||
|
|
36
|
+
"127.0.0.1";
|
|
37
|
+
const clientUserAgent = req.headers.get("user-agent");
|
|
38
|
+
|
|
39
|
+
const hashedUserValues = await hashUserData(user_data);
|
|
40
|
+
const enrichedData: Record<string, string | undefined> = {};
|
|
41
|
+
|
|
42
|
+
Object.entries(hashedUserValues).forEach(([key, value]) => {
|
|
43
|
+
if (value) enrichedData[key] = value;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (fbc || user_data.fbc) enrichedData.fbc = fbc || user_data.fbc;
|
|
47
|
+
if (fbp || user_data.fbp) enrichedData.fbp = fbp || user_data.fbp;
|
|
48
|
+
if (user_data.externalId) enrichedData.external_id = user_data.externalId;
|
|
49
|
+
if (user_data.facebookLoginId)
|
|
50
|
+
enrichedData.facebook_login_id = user_data.facebookLoginId;
|
|
51
|
+
|
|
52
|
+
enrichedData.client_ip_address = clientIp;
|
|
53
|
+
if (clientUserAgent) enrichedData.client_user_agent = clientUserAgent;
|
|
54
|
+
|
|
55
|
+
const cleanedUserData = Object.fromEntries(
|
|
56
|
+
Object.entries(enrichedData).filter(([_, v]) => v !== undefined && v !== ""),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const event: CAPIEvent = {
|
|
60
|
+
event_name,
|
|
61
|
+
event_time,
|
|
62
|
+
event_id,
|
|
63
|
+
event_source_url,
|
|
64
|
+
action_source,
|
|
65
|
+
user_data: cleanedUserData as any,
|
|
66
|
+
custom_data,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const payload: any = { data: [event] };
|
|
70
|
+
if (test_event_code) payload.test_event_code = test_event_code;
|
|
71
|
+
|
|
72
|
+
const response = await fetch(
|
|
73
|
+
`https://graph.facebook.com/v18.0/${pixelId}/events?access_token=${accessToken}`,
|
|
74
|
+
{
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: { "Content-Type": "application/json" },
|
|
77
|
+
body: JSON.stringify(payload),
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const result = await response.json();
|
|
82
|
+
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
return NextResponse.json(result, { status: response.status });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return NextResponse.json(result);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error("CAPI Runtime Error:", error);
|
|
90
|
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
|
91
|
+
}
|
|
92
|
+
}
|
package/src/lib/gtm.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { GA4Item, GTMUserData } from "./types";
|
|
4
|
+
|
|
5
|
+
declare global {
|
|
6
|
+
interface Window {
|
|
7
|
+
dataLayer?: Record<string, unknown>[];
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function pushToDataLayer(payload: Record<string, unknown>) {
|
|
12
|
+
if (typeof window === "undefined") return;
|
|
13
|
+
window.dataLayer = window.dataLayer || [];
|
|
14
|
+
window.dataLayer.push(payload);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function pushEcommerceEvent(
|
|
18
|
+
event: string,
|
|
19
|
+
ecommerce: Record<string, unknown>,
|
|
20
|
+
extra?: Record<string, unknown>,
|
|
21
|
+
) {
|
|
22
|
+
pushToDataLayer({ ecommerce: null }); // clear previous ecommerce object
|
|
23
|
+
pushToDataLayer({ event, ecommerce, ...extra });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function contentsToGA4Items(
|
|
27
|
+
contents: Array<{
|
|
28
|
+
id: string;
|
|
29
|
+
quantity: number;
|
|
30
|
+
item_price: number;
|
|
31
|
+
item_name?: string;
|
|
32
|
+
item_brand?: string;
|
|
33
|
+
item_category?: string;
|
|
34
|
+
}>,
|
|
35
|
+
fallbackItemName?: string,
|
|
36
|
+
fallbackItemCategory?: string,
|
|
37
|
+
): GA4Item[] {
|
|
38
|
+
return contents.map((item) => ({
|
|
39
|
+
item_id: item.id,
|
|
40
|
+
...(item.item_name || fallbackItemName
|
|
41
|
+
? { item_name: item.item_name || fallbackItemName }
|
|
42
|
+
: {}),
|
|
43
|
+
...(item.item_brand ? { item_brand: item.item_brand } : {}),
|
|
44
|
+
...(item.item_category || fallbackItemCategory
|
|
45
|
+
? { item_category: item.item_category || fallbackItemCategory }
|
|
46
|
+
: {}),
|
|
47
|
+
quantity: item.quantity,
|
|
48
|
+
price: item.item_price,
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function buildGTMUserData(user: any): GTMUserData | undefined {
|
|
53
|
+
if (!user) return undefined;
|
|
54
|
+
const data: GTMUserData = {};
|
|
55
|
+
if (user.email) data.email = user.email;
|
|
56
|
+
if (user.phone || user.mobileNumber)
|
|
57
|
+
data.phone_number = user.phone || user.mobileNumber;
|
|
58
|
+
if (user.firstName || user.first_name)
|
|
59
|
+
data.first_name = user.firstName || user.first_name;
|
|
60
|
+
if (user.lastName || user.last_name)
|
|
61
|
+
data.last_name = user.lastName || user.last_name;
|
|
62
|
+
if (user.type) data.type = user.type;
|
|
63
|
+
const addr = user.address || user.billing_address;
|
|
64
|
+
if (addr) {
|
|
65
|
+
data.address = {
|
|
66
|
+
...(addr.street && { street: addr.street }),
|
|
67
|
+
...(addr.city && { city: addr.city }),
|
|
68
|
+
...(addr.region || addr.state
|
|
69
|
+
? { region: addr.region || addr.state }
|
|
70
|
+
: {}),
|
|
71
|
+
...(addr.postal_code || addr.zip
|
|
72
|
+
? { postal_code: addr.postal_code || addr.zip }
|
|
73
|
+
: {}),
|
|
74
|
+
country: addr.country || "EG",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return data;
|
|
78
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { CAPIUserData } from "./types";
|
|
2
|
+
|
|
3
|
+
async function hashValue(value: string | undefined): Promise<string | undefined> {
|
|
4
|
+
if (!value || value === "") return undefined;
|
|
5
|
+
if (/^[a-f0-9]{64}$/i.test(value)) return value;
|
|
6
|
+
|
|
7
|
+
const normalized = value.toLowerCase().trim();
|
|
8
|
+
try {
|
|
9
|
+
const cryptoSubtle = globalThis.crypto?.subtle;
|
|
10
|
+
if (!cryptoSubtle) return normalized;
|
|
11
|
+
const msgUint8 = new TextEncoder().encode(normalized);
|
|
12
|
+
const hashBuffer = await cryptoSubtle.digest("SHA-256", msgUint8);
|
|
13
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
14
|
+
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
15
|
+
} catch {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function hashUserData(data: CAPIUserData) {
|
|
21
|
+
const [em, ph, fn, ln, ct, st, zp, country, db, ge] = await Promise.all([
|
|
22
|
+
hashValue(data.email),
|
|
23
|
+
hashValue(data.phone?.replace(/\D/g, "")),
|
|
24
|
+
hashValue(data.firstName),
|
|
25
|
+
hashValue(data.lastName),
|
|
26
|
+
hashValue(data.city),
|
|
27
|
+
hashValue(data.state),
|
|
28
|
+
hashValue(data.zip),
|
|
29
|
+
hashValue(data.country),
|
|
30
|
+
data.dateOfBirth ? hashValue(data.dateOfBirth) : undefined,
|
|
31
|
+
data.gender ? hashValue(data.gender) : undefined,
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
return { em, ph, fn, ln, ct, st, zp, country, db, ge };
|
|
35
|
+
}
|
package/src/lib/pixel.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
declare global {
|
|
2
|
+
interface Window {
|
|
3
|
+
fbq: (
|
|
4
|
+
command: string,
|
|
5
|
+
eventName: string,
|
|
6
|
+
params?: Record<string, unknown>,
|
|
7
|
+
options?: { eventID?: string },
|
|
8
|
+
) => void;
|
|
9
|
+
_fbq: unknown[];
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const getCookie = (name: string): string => {
|
|
14
|
+
if (typeof document === "undefined") return "";
|
|
15
|
+
const match = document.cookie.match(new RegExp("(^| )" + name + "=([^;]+)"));
|
|
16
|
+
return match ? match[2] : "";
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const getFbp = (): string => getCookie("_fbp");
|
|
20
|
+
|
|
21
|
+
export const getFbc = (): string => {
|
|
22
|
+
const cookieFbc = getCookie("_fbc");
|
|
23
|
+
if (cookieFbc) return cookieFbc;
|
|
24
|
+
|
|
25
|
+
if (typeof window !== "undefined") {
|
|
26
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
27
|
+
const fbclid = urlParams.get("fbclid");
|
|
28
|
+
if (fbclid) return `fb.1.${Date.now()}.${fbclid}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return "";
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function generateEventId(prefix: string): string {
|
|
35
|
+
return `${prefix}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getEnhancedUserData(user?: any) {
|
|
39
|
+
return {
|
|
40
|
+
email: user?.email,
|
|
41
|
+
phone: user?.mobileNumber || user?.phone,
|
|
42
|
+
firstName: user?.firstName || user?.name,
|
|
43
|
+
lastName: user?.lastName || user?.last_name,
|
|
44
|
+
city: user?.city,
|
|
45
|
+
state: user?.state,
|
|
46
|
+
zip: user?.zip,
|
|
47
|
+
country: user?.country || "EG",
|
|
48
|
+
externalId: user?.id?.toString() || user?.externalId,
|
|
49
|
+
dateOfBirth: user?.dateOfBirth || user?.dob,
|
|
50
|
+
gender: user?.gender,
|
|
51
|
+
facebookLoginId: user?.facebookId || user?.facebook_id,
|
|
52
|
+
fbc: getFbc(),
|
|
53
|
+
fbp: getFbp(),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function trackPageView(eventId?: string): void {
|
|
58
|
+
if (typeof window === "undefined" || !window.fbq) return;
|
|
59
|
+
const id = eventId || generateEventId("pageview");
|
|
60
|
+
// eventID must be in the 4th options param for Meta deduplication
|
|
61
|
+
window.fbq("track", "PageView", {}, { eventID: id });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function trackViewContent(
|
|
65
|
+
data: {
|
|
66
|
+
content_ids: string[];
|
|
67
|
+
content_name: string;
|
|
68
|
+
content_category?: string;
|
|
69
|
+
value: number;
|
|
70
|
+
currency: string;
|
|
71
|
+
},
|
|
72
|
+
eventId?: string,
|
|
73
|
+
): void {
|
|
74
|
+
if (typeof window === "undefined" || !window.fbq) return;
|
|
75
|
+
const id = eventId || generateEventId("vc");
|
|
76
|
+
window.fbq("track", "ViewContent", { content_type: "product", ...data }, { eventID: id });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function trackAddToCart(
|
|
80
|
+
data: {
|
|
81
|
+
content_ids: string[];
|
|
82
|
+
contents: Array<{ id: string; quantity: number; item_price: number }>;
|
|
83
|
+
value: number;
|
|
84
|
+
currency: string;
|
|
85
|
+
},
|
|
86
|
+
eventId?: string,
|
|
87
|
+
): void {
|
|
88
|
+
if (typeof window === "undefined" || !window.fbq) return;
|
|
89
|
+
const id = eventId || generateEventId("atc");
|
|
90
|
+
window.fbq("track", "AddToCart", { content_type: "product", ...data }, { eventID: id });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function trackPurchase(data: {
|
|
94
|
+
content_ids: string[];
|
|
95
|
+
contents: Array<{ id: string; quantity: number; item_price: number }>;
|
|
96
|
+
value: number;
|
|
97
|
+
currency: string;
|
|
98
|
+
num_items: number;
|
|
99
|
+
order_id: string;
|
|
100
|
+
}): void {
|
|
101
|
+
if (typeof window === "undefined" || !window.fbq) return;
|
|
102
|
+
window.fbq(
|
|
103
|
+
"track",
|
|
104
|
+
"Purchase",
|
|
105
|
+
{
|
|
106
|
+
value: data.value,
|
|
107
|
+
currency: data.currency,
|
|
108
|
+
content_ids: data.content_ids,
|
|
109
|
+
contents: data.contents,
|
|
110
|
+
content_type: "product",
|
|
111
|
+
num_items: data.num_items,
|
|
112
|
+
},
|
|
113
|
+
{ eventID: data.order_id },
|
|
114
|
+
);
|
|
115
|
+
}
|
package/src/lib/types.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export interface CAPIUserData {
|
|
2
|
+
email?: string;
|
|
3
|
+
phone?: string;
|
|
4
|
+
firstName?: string;
|
|
5
|
+
lastName?: string;
|
|
6
|
+
city?: string;
|
|
7
|
+
state?: string;
|
|
8
|
+
zip?: string;
|
|
9
|
+
country?: string;
|
|
10
|
+
externalId?: string;
|
|
11
|
+
dateOfBirth?: string;
|
|
12
|
+
gender?: string;
|
|
13
|
+
facebookLoginId?: string;
|
|
14
|
+
fbc?: string;
|
|
15
|
+
fbp?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CAPIEvent {
|
|
19
|
+
event_name: string;
|
|
20
|
+
event_time: number;
|
|
21
|
+
event_id?: string;
|
|
22
|
+
event_source_url?: string;
|
|
23
|
+
action_source: string;
|
|
24
|
+
user_data: Record<string, string | undefined>;
|
|
25
|
+
custom_data?: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface GA4Item {
|
|
29
|
+
item_id: string;
|
|
30
|
+
item_name?: string;
|
|
31
|
+
item_brand?: string;
|
|
32
|
+
item_category?: string;
|
|
33
|
+
item_list_name?: string;
|
|
34
|
+
quantity?: number;
|
|
35
|
+
price?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface GTMUserData {
|
|
39
|
+
email?: string;
|
|
40
|
+
phone_number?: string;
|
|
41
|
+
first_name?: string;
|
|
42
|
+
last_name?: string;
|
|
43
|
+
type?: string;
|
|
44
|
+
address?: {
|
|
45
|
+
street?: string;
|
|
46
|
+
city?: string;
|
|
47
|
+
region?: string;
|
|
48
|
+
postal_code?: string;
|
|
49
|
+
country?: string;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface BrowserPixelData {
|
|
54
|
+
content_ids?: string[];
|
|
55
|
+
content_name?: string;
|
|
56
|
+
content_category?: string;
|
|
57
|
+
value?: number;
|
|
58
|
+
currency?: string;
|
|
59
|
+
contents?: Array<{ id: string; quantity: number; item_price: number }>;
|
|
60
|
+
num_items?: number;
|
|
61
|
+
order_id?: string;
|
|
62
|
+
event_id?: string;
|
|
63
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { POST } from "./lib/capiHandler";
|