@uptrademedia/site-kit 1.0.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/README.md +305 -0
- package/dist/analytics/index.js +88 -0
- package/dist/analytics/index.js.map +1 -0
- package/dist/analytics/index.mjs +70 -0
- package/dist/analytics/index.mjs.map +1 -0
- package/dist/api-N35S3EES.js +57 -0
- package/dist/api-N35S3EES.js.map +1 -0
- package/dist/api-SYBTK7Z7.mjs +4 -0
- package/dist/api-SYBTK7Z7.mjs.map +1 -0
- package/dist/blog/index.js +200 -0
- package/dist/blog/index.js.map +1 -0
- package/dist/blog/index.mjs +194 -0
- package/dist/blog/index.mjs.map +1 -0
- package/dist/chunk-3MUOUXHV.js +3721 -0
- package/dist/chunk-3MUOUXHV.js.map +1 -0
- package/dist/chunk-4HVYXYQL 2.mjs +255 -0
- package/dist/chunk-4HVYXYQL.mjs +255 -0
- package/dist/chunk-4HVYXYQL.mjs.map +1 -0
- package/dist/chunk-7H6I3ECV.mjs +120 -0
- package/dist/chunk-7H6I3ECV.mjs.map +1 -0
- package/dist/chunk-COI6GOX2.mjs +3679 -0
- package/dist/chunk-COI6GOX2.mjs.map +1 -0
- package/dist/chunk-EQCVQC35.js +35 -0
- package/dist/chunk-EQCVQC35.js 2.map +1 -0
- package/dist/chunk-EQCVQC35.js.map +1 -0
- package/dist/chunk-FEBYQGY4 2.mjs +251 -0
- package/dist/chunk-FEBYQGY4.mjs +251 -0
- package/dist/chunk-FEBYQGY4.mjs.map +1 -0
- package/dist/chunk-FKVJOT2F.mjs +796 -0
- package/dist/chunk-FKVJOT2F.mjs.map +1 -0
- package/dist/chunk-GQ6ZOU2N.mjs +134 -0
- package/dist/chunk-GQ6ZOU2N.mjs.map +1 -0
- package/dist/chunk-HCFPU7TU.js +137 -0
- package/dist/chunk-HCFPU7TU.js.map +1 -0
- package/dist/chunk-NYKRE2FL 2.mjs +31 -0
- package/dist/chunk-NYKRE2FL.mjs +31 -0
- package/dist/chunk-NYKRE2FL.mjs 2.map +1 -0
- package/dist/chunk-NYKRE2FL.mjs.map +1 -0
- package/dist/chunk-QP5NCO2E.js +133 -0
- package/dist/chunk-QP5NCO2E.js.map +1 -0
- package/dist/chunk-RV7H3I6J.js +255 -0
- package/dist/chunk-RV7H3I6J.js 2.map +1 -0
- package/dist/chunk-RV7H3I6J.js.map +1 -0
- package/dist/chunk-SBVEYCSV.js +140 -0
- package/dist/chunk-SBVEYCSV.js.map +1 -0
- package/dist/chunk-TUKGA3UK.js +257 -0
- package/dist/chunk-TUKGA3UK.js 2.map +1 -0
- package/dist/chunk-TUKGA3UK.js.map +1 -0
- package/dist/chunk-V3F5J6CV.js +801 -0
- package/dist/chunk-V3F5J6CV.js.map +1 -0
- package/dist/chunk-WPSRS352.mjs +135 -0
- package/dist/chunk-WPSRS352.mjs.map +1 -0
- package/dist/commerce/index.js +157 -0
- package/dist/commerce/index.js.map +1 -0
- package/dist/commerce/index.mjs +4 -0
- package/dist/commerce/index.mjs.map +1 -0
- package/dist/commerce/server.js +186 -0
- package/dist/commerce/server.js.map +1 -0
- package/dist/commerce/server.mjs +176 -0
- package/dist/commerce/server.mjs.map +1 -0
- package/dist/engage/index.js +50 -0
- package/dist/engage/index.js.map +1 -0
- package/dist/engage/index.mjs +44 -0
- package/dist/engage/index.mjs.map +1 -0
- package/dist/forms/index.js +1053 -0
- package/dist/forms/index.js.map +1 -0
- package/dist/forms/index.mjs +1035 -0
- package/dist/forms/index.mjs.map +1 -0
- package/dist/generators-7Y5ABRYV 2.mjs +161 -0
- package/dist/generators-7Y5ABRYV.mjs +161 -0
- package/dist/generators-7Y5ABRYV.mjs 2.map +1 -0
- package/dist/generators-7Y5ABRYV.mjs.map +1 -0
- package/dist/generators-GWIYCA5M.js +171 -0
- package/dist/generators-GWIYCA5M.js 2.map +1 -0
- package/dist/generators-GWIYCA5M.js.map +1 -0
- package/dist/index 2.mjs +74 -0
- package/dist/index.js +326 -0
- package/dist/index.js 2.map +1 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +222 -0
- package/dist/index.mjs.map +1 -0
- package/dist/migrator-V6KS75EA 2.mjs +265 -0
- package/dist/migrator-V6KS75EA.mjs +265 -0
- package/dist/migrator-V6KS75EA.mjs 2.map +1 -0
- package/dist/migrator-V6KS75EA.mjs.map +1 -0
- package/dist/migrator-XKM7YQCY.js +272 -0
- package/dist/migrator-XKM7YQCY.js 2.map +1 -0
- package/dist/migrator-XKM7YQCY.js.map +1 -0
- package/dist/scanner-MF7P3CDE 2.mjs +14386 -0
- package/dist/scanner-MF7P3CDE.mjs +14386 -0
- package/dist/scanner-MF7P3CDE.mjs 2.map +1 -0
- package/dist/scanner-MF7P3CDE.mjs.map +1 -0
- package/dist/scanner-NT6YG4TD 2.js +14397 -0
- package/dist/scanner-NT6YG4TD.js +14397 -0
- package/dist/scanner-NT6YG4TD.js 2.map +1 -0
- package/dist/scanner-NT6YG4TD.js.map +1 -0
- package/dist/seo/index.js +447 -0
- package/dist/seo/index.js.map +1 -0
- package/dist/seo/index.mjs +411 -0
- package/dist/seo/index.mjs.map +1 -0
- package/dist/seo/server.js +66 -0
- package/dist/seo/server.js.map +1 -0
- package/dist/seo/server.mjs +5 -0
- package/dist/seo/server.mjs.map +1 -0
- package/dist/setup/index.js +1050 -0
- package/dist/setup/index.js.map +1 -0
- package/dist/setup/index.mjs +1046 -0
- package/dist/setup/index.mjs.map +1 -0
- package/dist/sitemap/index.js +212 -0
- package/dist/sitemap/index.js.map +1 -0
- package/dist/sitemap/index.mjs +206 -0
- package/dist/sitemap/index.mjs.map +1 -0
- package/dist/web-vitals-BH55V7EJ.js +252 -0
- package/dist/web-vitals-BH55V7EJ.js 2.map +1 -0
- package/dist/web-vitals-BH55V7EJ.js.map +1 -0
- package/dist/web-vitals-RJYPWAR3 2.mjs +241 -0
- package/dist/web-vitals-RJYPWAR3.mjs +241 -0
- package/dist/web-vitals-RJYPWAR3.mjs 2.map +1 -0
- package/dist/web-vitals-RJYPWAR3.mjs.map +1 -0
- package/package.json +118 -0
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
import { createContext, useEffect, useRef, useCallback, useMemo, useContext } from 'react';
|
|
2
|
+
import { usePathname, useSearchParams } from 'next/navigation';
|
|
3
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
// src/analytics/WebVitals.tsx
|
|
6
|
+
function getApiConfig() {
|
|
7
|
+
const apiUrl = typeof window !== "undefined" ? window.__SITE_KIT_API_URL__ || "https://api.uptrademedia.com" : "https://api.uptrademedia.com";
|
|
8
|
+
const apiKey = typeof window !== "undefined" ? window.__SITE_KIT_API_KEY__ : void 0;
|
|
9
|
+
return { apiUrl, apiKey };
|
|
10
|
+
}
|
|
11
|
+
function WebVitals({ apiUrl: propApiUrl, apiKey: propApiKey, debug = false }) {
|
|
12
|
+
const pathname = usePathname();
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
import('./web-vitals-RJYPWAR3.mjs').then(({ onCLS, onLCP, onTTFB, onINP, onFCP }) => {
|
|
15
|
+
const vitals = {};
|
|
16
|
+
let reported = false;
|
|
17
|
+
const reportVitals = async () => {
|
|
18
|
+
if (reported) return;
|
|
19
|
+
if (Object.keys(vitals).length === 0) return;
|
|
20
|
+
reported = true;
|
|
21
|
+
const { apiUrl: globalApiUrl, apiKey: globalApiKey } = getApiConfig();
|
|
22
|
+
const apiUrl = propApiUrl || globalApiUrl;
|
|
23
|
+
const apiKey = propApiKey || globalApiKey;
|
|
24
|
+
if (!apiKey) {
|
|
25
|
+
if (debug) console.warn("[Analytics] No API key configured for Web Vitals");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
for (const [name, value] of Object.entries(vitals)) {
|
|
29
|
+
const data = {
|
|
30
|
+
pagePath: pathname,
|
|
31
|
+
metricName: name,
|
|
32
|
+
metricValue: value,
|
|
33
|
+
metricRating: getRating(name, value)
|
|
34
|
+
};
|
|
35
|
+
if (debug) {
|
|
36
|
+
console.log("[Analytics] Web Vital:", data);
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
await fetch(`${apiUrl}/api/public/analytics/web-vitals`, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: {
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
"x-api-key": apiKey
|
|
44
|
+
},
|
|
45
|
+
body: JSON.stringify(data)
|
|
46
|
+
});
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (debug) console.error("[Analytics] Error reporting Web Vital:", error);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
onLCP((metric) => {
|
|
53
|
+
vitals.LCP = metric.value;
|
|
54
|
+
if (debug) console.log("[Analytics] LCP:", metric.value);
|
|
55
|
+
});
|
|
56
|
+
onCLS((metric) => {
|
|
57
|
+
vitals.CLS = metric.value;
|
|
58
|
+
if (debug) console.log("[Analytics] CLS:", metric.value);
|
|
59
|
+
});
|
|
60
|
+
onTTFB((metric) => {
|
|
61
|
+
vitals.TTFB = metric.value;
|
|
62
|
+
if (debug) console.log("[Analytics] TTFB:", metric.value);
|
|
63
|
+
});
|
|
64
|
+
onINP((metric) => {
|
|
65
|
+
vitals.INP = metric.value;
|
|
66
|
+
if (debug) console.log("[Analytics] INP:", metric.value);
|
|
67
|
+
});
|
|
68
|
+
onFCP((metric) => {
|
|
69
|
+
vitals.FCP = metric.value;
|
|
70
|
+
if (debug) console.log("[Analytics] FCP:", metric.value);
|
|
71
|
+
});
|
|
72
|
+
const handleVisibilityChange = () => {
|
|
73
|
+
if (document.visibilityState === "hidden") {
|
|
74
|
+
reportVitals();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
78
|
+
const timeout = setTimeout(reportVitals, 1e4);
|
|
79
|
+
return () => {
|
|
80
|
+
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
81
|
+
clearTimeout(timeout);
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
}, [pathname, propApiUrl, propApiKey, debug]);
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
function getRating(name, value) {
|
|
88
|
+
const thresholds = {
|
|
89
|
+
LCP: [2500, 4e3],
|
|
90
|
+
CLS: [0.1, 0.25],
|
|
91
|
+
TTFB: [800, 1800],
|
|
92
|
+
INP: [200, 500],
|
|
93
|
+
FCP: [1800, 3e3]
|
|
94
|
+
};
|
|
95
|
+
const [good, poor] = thresholds[name] || [0, 0];
|
|
96
|
+
if (value <= good) return "good";
|
|
97
|
+
if (value <= poor) return "needs-improvement";
|
|
98
|
+
return "poor";
|
|
99
|
+
}
|
|
100
|
+
var AnalyticsContext = createContext(null);
|
|
101
|
+
function generateId() {
|
|
102
|
+
return crypto.randomUUID();
|
|
103
|
+
}
|
|
104
|
+
function scheduleIdleTask(callback, timeout = 2e3) {
|
|
105
|
+
if (typeof window !== "undefined" && "requestIdleCallback" in window) {
|
|
106
|
+
window.requestIdleCallback(
|
|
107
|
+
callback,
|
|
108
|
+
{ timeout }
|
|
109
|
+
);
|
|
110
|
+
} else {
|
|
111
|
+
setTimeout(callback, 0);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function getOrCreateVisitorId() {
|
|
115
|
+
if (typeof window === "undefined") return "";
|
|
116
|
+
const key = "_uptrade_vid";
|
|
117
|
+
let visitorId = localStorage.getItem(key);
|
|
118
|
+
if (!visitorId) {
|
|
119
|
+
visitorId = generateId();
|
|
120
|
+
localStorage.setItem(key, visitorId);
|
|
121
|
+
}
|
|
122
|
+
return visitorId;
|
|
123
|
+
}
|
|
124
|
+
function getSessionId(timeout) {
|
|
125
|
+
if (typeof window === "undefined") return "";
|
|
126
|
+
const key = "_uptrade_sid";
|
|
127
|
+
const timeKey = "_uptrade_stime";
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
const timeoutMs = timeout * 60 * 1e3;
|
|
130
|
+
const existingSession = sessionStorage.getItem(key);
|
|
131
|
+
const lastActivity = sessionStorage.getItem(timeKey);
|
|
132
|
+
if (existingSession && lastActivity) {
|
|
133
|
+
const elapsed = now - parseInt(lastActivity, 10);
|
|
134
|
+
if (elapsed < timeoutMs) {
|
|
135
|
+
sessionStorage.setItem(timeKey, now.toString());
|
|
136
|
+
return existingSession;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const newSession = generateId();
|
|
140
|
+
sessionStorage.setItem(key, newSession);
|
|
141
|
+
sessionStorage.setItem(timeKey, now.toString());
|
|
142
|
+
return newSession;
|
|
143
|
+
}
|
|
144
|
+
function getDeviceType() {
|
|
145
|
+
if (typeof window === "undefined") return "desktop";
|
|
146
|
+
const ua = navigator.userAgent;
|
|
147
|
+
if (/tablet|ipad|playbook|silk/i.test(ua)) return "tablet";
|
|
148
|
+
if (/mobile|iphone|ipod|android|blackberry|opera mini|iemobile/i.test(ua)) return "mobile";
|
|
149
|
+
return "desktop";
|
|
150
|
+
}
|
|
151
|
+
function getBrowser() {
|
|
152
|
+
if (typeof window === "undefined") return "unknown";
|
|
153
|
+
const ua = navigator.userAgent;
|
|
154
|
+
if (ua.includes("Firefox")) return "Firefox";
|
|
155
|
+
if (ua.includes("Edg")) return "Edge";
|
|
156
|
+
if (ua.includes("Chrome")) return "Chrome";
|
|
157
|
+
if (ua.includes("Safari")) return "Safari";
|
|
158
|
+
if (ua.includes("Opera") || ua.includes("OPR")) return "Opera";
|
|
159
|
+
return "Other";
|
|
160
|
+
}
|
|
161
|
+
function getOS() {
|
|
162
|
+
if (typeof window === "undefined") return "unknown";
|
|
163
|
+
const ua = navigator.userAgent;
|
|
164
|
+
if (ua.includes("Windows")) return "Windows";
|
|
165
|
+
if (ua.includes("Mac OS X") || ua.includes("Macintosh")) return "macOS";
|
|
166
|
+
if (ua.includes("iPhone") || ua.includes("iPad")) return "iOS";
|
|
167
|
+
if (ua.includes("Android")) return "Android";
|
|
168
|
+
if (ua.includes("Linux")) return "Linux";
|
|
169
|
+
if (ua.includes("CrOS")) return "ChromeOS";
|
|
170
|
+
return "Other";
|
|
171
|
+
}
|
|
172
|
+
function getUserAgent() {
|
|
173
|
+
if (typeof window === "undefined") return "";
|
|
174
|
+
return navigator.userAgent;
|
|
175
|
+
}
|
|
176
|
+
function getUTMParams() {
|
|
177
|
+
if (typeof window === "undefined") return {};
|
|
178
|
+
const params = new URLSearchParams(window.location.search);
|
|
179
|
+
const utmParams = {};
|
|
180
|
+
for (const key of ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"]) {
|
|
181
|
+
const value = params.get(key);
|
|
182
|
+
if (value) utmParams[key] = value;
|
|
183
|
+
}
|
|
184
|
+
return utmParams;
|
|
185
|
+
}
|
|
186
|
+
function getPageMetadata() {
|
|
187
|
+
if (typeof document === "undefined") return {};
|
|
188
|
+
const getMeta = (name) => {
|
|
189
|
+
const el = document.querySelector(`meta[name="${name}"], meta[property="${name}"]`);
|
|
190
|
+
return el?.getAttribute("content") || null;
|
|
191
|
+
};
|
|
192
|
+
const getCanonical = () => {
|
|
193
|
+
const el = document.querySelector('link[rel="canonical"]');
|
|
194
|
+
return el?.getAttribute("href") || null;
|
|
195
|
+
};
|
|
196
|
+
const getRobots = () => {
|
|
197
|
+
return getMeta("robots");
|
|
198
|
+
};
|
|
199
|
+
const getH1 = () => {
|
|
200
|
+
const h1 = document.querySelector("h1");
|
|
201
|
+
return h1?.textContent?.trim() || null;
|
|
202
|
+
};
|
|
203
|
+
const getH1Count = () => {
|
|
204
|
+
return document.querySelectorAll("h1").length;
|
|
205
|
+
};
|
|
206
|
+
const getWordCount = () => {
|
|
207
|
+
const main = document.querySelector('main, article, [role="main"]') || document.body;
|
|
208
|
+
const text = main.textContent || "";
|
|
209
|
+
return text.split(/\s+/).filter((w) => w.length > 0).length;
|
|
210
|
+
};
|
|
211
|
+
const getImageStats = () => {
|
|
212
|
+
const images = document.querySelectorAll("img");
|
|
213
|
+
let withoutAlt = 0;
|
|
214
|
+
images.forEach((img) => {
|
|
215
|
+
if (!img.alt || img.alt.trim() === "") withoutAlt++;
|
|
216
|
+
});
|
|
217
|
+
return { count: images.length, withoutAlt };
|
|
218
|
+
};
|
|
219
|
+
const getLinkStats = () => {
|
|
220
|
+
const links = document.querySelectorAll("a[href]");
|
|
221
|
+
const currentHost = window.location.host;
|
|
222
|
+
let internal = 0;
|
|
223
|
+
let external = 0;
|
|
224
|
+
const internalLinks = [];
|
|
225
|
+
const getLinkPosition = (el) => {
|
|
226
|
+
let current = el;
|
|
227
|
+
while (current) {
|
|
228
|
+
const tag = current.tagName?.toLowerCase();
|
|
229
|
+
const role = current.getAttribute("role");
|
|
230
|
+
if (tag === "header" || role === "banner") return "header";
|
|
231
|
+
if (tag === "nav" || role === "navigation") return "nav";
|
|
232
|
+
if (tag === "footer" || role === "contentinfo") return "footer";
|
|
233
|
+
if (tag === "aside" || role === "complementary") return "sidebar";
|
|
234
|
+
if (tag === "main" || tag === "article" || role === "main") return "content";
|
|
235
|
+
current = current.parentElement;
|
|
236
|
+
}
|
|
237
|
+
return "unknown";
|
|
238
|
+
};
|
|
239
|
+
links.forEach((link) => {
|
|
240
|
+
const href = link.getAttribute("href") || "";
|
|
241
|
+
const rel = link.getAttribute("rel") || "";
|
|
242
|
+
const isNofollow = rel.includes("nofollow");
|
|
243
|
+
let isInternal = false;
|
|
244
|
+
let targetPath = "";
|
|
245
|
+
if (href.startsWith("/") && !href.startsWith("//")) {
|
|
246
|
+
isInternal = true;
|
|
247
|
+
targetPath = href.split("?")[0].split("#")[0];
|
|
248
|
+
} else if (href.startsWith("#")) {
|
|
249
|
+
internal++;
|
|
250
|
+
return;
|
|
251
|
+
} else if (href.startsWith("http")) {
|
|
252
|
+
try {
|
|
253
|
+
const url = new URL(href);
|
|
254
|
+
if (url.host === currentHost) {
|
|
255
|
+
isInternal = true;
|
|
256
|
+
targetPath = url.pathname;
|
|
257
|
+
} else {
|
|
258
|
+
external++;
|
|
259
|
+
}
|
|
260
|
+
} catch {
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (isInternal && targetPath) {
|
|
264
|
+
internal++;
|
|
265
|
+
if (!targetPath.startsWith("/")) targetPath = "/" + targetPath;
|
|
266
|
+
if (targetPath !== "/" && targetPath.endsWith("/")) {
|
|
267
|
+
targetPath = targetPath.slice(0, -1);
|
|
268
|
+
}
|
|
269
|
+
internalLinks.push({
|
|
270
|
+
targetPath,
|
|
271
|
+
anchorText: (link.textContent || "").trim().slice(0, 200),
|
|
272
|
+
position: getLinkPosition(link),
|
|
273
|
+
isNofollow
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
return { internal, external, internalLinks };
|
|
278
|
+
};
|
|
279
|
+
const imageStats = getImageStats();
|
|
280
|
+
const linkStats = getLinkStats();
|
|
281
|
+
return {
|
|
282
|
+
metaDescription: getMeta("description"),
|
|
283
|
+
canonical: getCanonical(),
|
|
284
|
+
robots: getRobots(),
|
|
285
|
+
ogTitle: getMeta("og:title"),
|
|
286
|
+
ogDescription: getMeta("og:description"),
|
|
287
|
+
ogImage: getMeta("og:image"),
|
|
288
|
+
h1: getH1(),
|
|
289
|
+
h1Count: getH1Count(),
|
|
290
|
+
wordCount: getWordCount(),
|
|
291
|
+
imagesCount: imageStats.count,
|
|
292
|
+
imagesWithoutAlt: imageStats.withoutAlt,
|
|
293
|
+
internalLinks: linkStats.internal,
|
|
294
|
+
internalLinkTargets: linkStats.internalLinks,
|
|
295
|
+
// Full link graph data
|
|
296
|
+
externalLinks: linkStats.external
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function getApiConfig2() {
|
|
300
|
+
const apiUrl = typeof window !== "undefined" ? window.__SITE_KIT_API_URL__ || "https://api.uptrademedia.com" : "https://api.uptrademedia.com";
|
|
301
|
+
const apiKey = typeof window !== "undefined" ? window.__SITE_KIT_API_KEY__ : void 0;
|
|
302
|
+
return { apiUrl, apiKey };
|
|
303
|
+
}
|
|
304
|
+
function AnalyticsProvider({
|
|
305
|
+
children,
|
|
306
|
+
apiUrl: propApiUrl,
|
|
307
|
+
apiKey: propApiKey,
|
|
308
|
+
trackPageViews = true,
|
|
309
|
+
trackWebVitals = true,
|
|
310
|
+
trackScrollDepth = true,
|
|
311
|
+
trackClicks = true,
|
|
312
|
+
trackJourneys = true,
|
|
313
|
+
// NEW: Enable journey tracking by default
|
|
314
|
+
sessionTimeout = 30,
|
|
315
|
+
excludePaths = [],
|
|
316
|
+
validateAgainstSitemap = true,
|
|
317
|
+
debug = false
|
|
318
|
+
}) {
|
|
319
|
+
const pathname = usePathname();
|
|
320
|
+
const searchParams = useSearchParams();
|
|
321
|
+
const visitorIdRef = useRef("");
|
|
322
|
+
const sessionIdRef = useRef("");
|
|
323
|
+
const lastPathRef = useRef("");
|
|
324
|
+
const validPathsRef = useRef(null);
|
|
325
|
+
const journeyStartTimeRef = useRef(0);
|
|
326
|
+
const pageEnterTimeRef = useRef(0);
|
|
327
|
+
const currentScrollDepthRef = useRef(0);
|
|
328
|
+
useEffect(() => {
|
|
329
|
+
visitorIdRef.current = getOrCreateVisitorId();
|
|
330
|
+
sessionIdRef.current = getSessionId(sessionTimeout);
|
|
331
|
+
}, [sessionTimeout]);
|
|
332
|
+
useEffect(() => {
|
|
333
|
+
if (!validateAgainstSitemap) {
|
|
334
|
+
if (debug) console.log("[Analytics] Page validation disabled");
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const fetchValidPages = async () => {
|
|
338
|
+
const { apiUrl: globalApiUrl, apiKey: globalApiKey } = getApiConfig2();
|
|
339
|
+
const apiUrl = propApiUrl || globalApiUrl;
|
|
340
|
+
const apiKey = propApiKey || globalApiKey;
|
|
341
|
+
if (!apiKey) {
|
|
342
|
+
if (debug) console.warn("[Analytics] No API key for page validation");
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
const response = await fetch(`${apiUrl}/api/public/seo/pages`, {
|
|
347
|
+
method: "GET",
|
|
348
|
+
headers: {
|
|
349
|
+
"Content-Type": "application/json",
|
|
350
|
+
"x-api-key": apiKey
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
if (response.ok) {
|
|
354
|
+
const data = await response.json();
|
|
355
|
+
const pages = data?.pages || [];
|
|
356
|
+
validPathsRef.current = new Set(
|
|
357
|
+
pages.map((p) => p.path).filter(Boolean)
|
|
358
|
+
);
|
|
359
|
+
if (debug) {
|
|
360
|
+
console.log("[Analytics] Loaded", validPathsRef.current.size, "valid pages from seo_pages");
|
|
361
|
+
}
|
|
362
|
+
} else if (debug) {
|
|
363
|
+
console.error("[Analytics] Pages fetch failed:", response.statusText);
|
|
364
|
+
}
|
|
365
|
+
} catch (error) {
|
|
366
|
+
if (debug) console.error("[Analytics] Error fetching valid pages:", error);
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
fetchValidPages();
|
|
370
|
+
}, [propApiUrl, propApiKey, validateAgainstSitemap, debug]);
|
|
371
|
+
useEffect(() => {
|
|
372
|
+
if (!trackPageViews) return;
|
|
373
|
+
if (!pathname) return;
|
|
374
|
+
if (excludePaths.some((p) => pathname.startsWith(p))) return;
|
|
375
|
+
if (pathname === lastPathRef.current) return;
|
|
376
|
+
if (validateAgainstSitemap) {
|
|
377
|
+
if (validPathsRef.current && validPathsRef.current.size > 0) {
|
|
378
|
+
if (!validPathsRef.current.has(pathname)) {
|
|
379
|
+
if (debug) {
|
|
380
|
+
console.log("[Analytics] Skipping unregistered path:", pathname);
|
|
381
|
+
}
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
} else if (debug) {
|
|
385
|
+
console.log("[Analytics] Sitemap not yet loaded, tracking anyway:", pathname);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
lastPathRef.current = pathname;
|
|
389
|
+
const trackPageView = async () => {
|
|
390
|
+
const { apiUrl: globalApiUrl, apiKey: globalApiKey } = getApiConfig2();
|
|
391
|
+
const apiUrl = propApiUrl || globalApiUrl;
|
|
392
|
+
const apiKey = propApiKey || globalApiKey;
|
|
393
|
+
if (!apiKey) {
|
|
394
|
+
if (debug) console.warn("[Analytics] No API key configured");
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const utmParams = getUTMParams();
|
|
398
|
+
const pageMetadata = getPageMetadata();
|
|
399
|
+
const pageView = {
|
|
400
|
+
sessionId: sessionIdRef.current,
|
|
401
|
+
visitorId: visitorIdRef.current,
|
|
402
|
+
pagePath: pathname,
|
|
403
|
+
pageTitle: document.title,
|
|
404
|
+
referrer: document.referrer || null,
|
|
405
|
+
deviceType: getDeviceType(),
|
|
406
|
+
browser: getBrowser(),
|
|
407
|
+
os: getOS(),
|
|
408
|
+
userAgent: getUserAgent(),
|
|
409
|
+
utmSource: utmParams.utm_source,
|
|
410
|
+
utmMedium: utmParams.utm_medium,
|
|
411
|
+
utmCampaign: utmParams.utm_campaign,
|
|
412
|
+
utmTerm: utmParams.utm_term,
|
|
413
|
+
utmContent: utmParams.utm_content,
|
|
414
|
+
// SEO enrichment data - updates seo_pages
|
|
415
|
+
seo: pageMetadata
|
|
416
|
+
};
|
|
417
|
+
if (debug) {
|
|
418
|
+
console.log("[Analytics] Page view:", pageView);
|
|
419
|
+
}
|
|
420
|
+
try {
|
|
421
|
+
const response = await fetch(`${apiUrl}/api/public/analytics/page-view`, {
|
|
422
|
+
method: "POST",
|
|
423
|
+
headers: {
|
|
424
|
+
"Content-Type": "application/json",
|
|
425
|
+
"x-api-key": apiKey
|
|
426
|
+
},
|
|
427
|
+
body: JSON.stringify(pageView)
|
|
428
|
+
});
|
|
429
|
+
if (!response.ok && debug) {
|
|
430
|
+
console.error("[Analytics] Error tracking page view:", response.statusText);
|
|
431
|
+
}
|
|
432
|
+
} catch (error) {
|
|
433
|
+
if (debug) console.error("[Analytics] Error tracking page view:", error);
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
scheduleIdleTask(() => trackPageView());
|
|
437
|
+
}, [pathname, searchParams, propApiUrl, propApiKey, trackPageViews, excludePaths, debug]);
|
|
438
|
+
useEffect(() => {
|
|
439
|
+
if (!trackJourneys) return;
|
|
440
|
+
if (!pathname) return;
|
|
441
|
+
if (typeof window === "undefined") return;
|
|
442
|
+
const { apiUrl: globalApiUrl, apiKey: globalApiKey } = getApiConfig2();
|
|
443
|
+
const apiUrl = propApiUrl || globalApiUrl;
|
|
444
|
+
const apiKey = propApiKey || globalApiKey;
|
|
445
|
+
if (!apiKey) return;
|
|
446
|
+
const now = Date.now();
|
|
447
|
+
const previousPath = lastPathRef.current;
|
|
448
|
+
const previousDuration = pageEnterTimeRef.current > 0 ? Math.round((now - pageEnterTimeRef.current) / 1e3) : 0;
|
|
449
|
+
const previousScrollDepth = currentScrollDepthRef.current;
|
|
450
|
+
pageEnterTimeRef.current = now;
|
|
451
|
+
currentScrollDepthRef.current = 0;
|
|
452
|
+
const isNewSession = !previousPath || journeyStartTimeRef.current === 0;
|
|
453
|
+
if (isNewSession) {
|
|
454
|
+
journeyStartTimeRef.current = now;
|
|
455
|
+
}
|
|
456
|
+
const trackJourneyStep = async () => {
|
|
457
|
+
const sessionData = {
|
|
458
|
+
sessionId: sessionIdRef.current,
|
|
459
|
+
visitorId: visitorIdRef.current,
|
|
460
|
+
action: isNewSession ? "start" : "update",
|
|
461
|
+
lastPage: pathname,
|
|
462
|
+
userAgent: getUserAgent(),
|
|
463
|
+
// Journey step data
|
|
464
|
+
journeyStep: {
|
|
465
|
+
page: pathname,
|
|
466
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
if (!isNewSession && previousDuration > 0) {
|
|
470
|
+
sessionData.previousPageDuration = previousDuration;
|
|
471
|
+
sessionData.previousPageScrollDepth = previousScrollDepth;
|
|
472
|
+
}
|
|
473
|
+
if (isNewSession) {
|
|
474
|
+
sessionData.firstPage = pathname;
|
|
475
|
+
const utmParams = getUTMParams();
|
|
476
|
+
sessionData.referrer = document.referrer || null;
|
|
477
|
+
sessionData.utmSource = utmParams.utm_source;
|
|
478
|
+
sessionData.utmMedium = utmParams.utm_medium;
|
|
479
|
+
sessionData.utmCampaign = utmParams.utm_campaign;
|
|
480
|
+
sessionData.utmTerm = utmParams.utm_term;
|
|
481
|
+
sessionData.utmContent = utmParams.utm_content;
|
|
482
|
+
sessionData.screenWidth = window.screen.width;
|
|
483
|
+
sessionData.screenHeight = window.screen.height;
|
|
484
|
+
}
|
|
485
|
+
if (debug) {
|
|
486
|
+
console.log("[Analytics] Journey step:", sessionData);
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
await fetch(`${apiUrl}/api/public/analytics/session`, {
|
|
490
|
+
method: "POST",
|
|
491
|
+
headers: {
|
|
492
|
+
"Content-Type": "application/json",
|
|
493
|
+
"x-api-key": apiKey
|
|
494
|
+
},
|
|
495
|
+
body: JSON.stringify(sessionData)
|
|
496
|
+
});
|
|
497
|
+
} catch (error) {
|
|
498
|
+
if (debug) console.error("[Analytics] Error tracking journey:", error);
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
scheduleIdleTask(() => trackJourneyStep());
|
|
502
|
+
const handleUnload = () => {
|
|
503
|
+
const duration = Math.round((Date.now() - journeyStartTimeRef.current) / 1e3);
|
|
504
|
+
const payload = JSON.stringify({
|
|
505
|
+
sessionId: sessionIdRef.current,
|
|
506
|
+
action: "end",
|
|
507
|
+
duration,
|
|
508
|
+
lastPage: pathname,
|
|
509
|
+
previousPageDuration: Math.round((Date.now() - pageEnterTimeRef.current) / 1e3),
|
|
510
|
+
previousPageScrollDepth: currentScrollDepthRef.current
|
|
511
|
+
});
|
|
512
|
+
if (navigator.sendBeacon) {
|
|
513
|
+
const blob = new Blob([payload], { type: "application/json" });
|
|
514
|
+
navigator.sendBeacon(
|
|
515
|
+
`${apiUrl}/api/public/analytics/session?key=${encodeURIComponent(apiKey)}`,
|
|
516
|
+
blob
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
window.addEventListener("beforeunload", handleUnload);
|
|
521
|
+
return () => {
|
|
522
|
+
window.removeEventListener("beforeunload", handleUnload);
|
|
523
|
+
};
|
|
524
|
+
}, [pathname, propApiUrl, propApiKey, trackJourneys, debug]);
|
|
525
|
+
useEffect(() => {
|
|
526
|
+
if (typeof window === "undefined") return;
|
|
527
|
+
if (debug) console.log("[Analytics] Scroll tracking setup:", { trackScrollDepth, hasApiKey: !!propApiKey });
|
|
528
|
+
if (!trackScrollDepth) return;
|
|
529
|
+
const { apiUrl: globalApiUrl, apiKey: globalApiKey } = getApiConfig2();
|
|
530
|
+
const apiUrl = propApiUrl || globalApiUrl;
|
|
531
|
+
const apiKey = propApiKey || globalApiKey;
|
|
532
|
+
if (!apiKey) {
|
|
533
|
+
if (debug) console.warn("[Analytics] Scroll tracking disabled - no API key");
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
if (debug) console.log("[Analytics] Scroll tracking enabled for:", pathname);
|
|
537
|
+
let maxDepth = 0;
|
|
538
|
+
let startTime = Date.now();
|
|
539
|
+
let milestone25 = null;
|
|
540
|
+
let milestone50 = null;
|
|
541
|
+
let milestone75 = null;
|
|
542
|
+
let milestone100 = null;
|
|
543
|
+
let hasTracked = false;
|
|
544
|
+
const calculateScrollDepth = () => {
|
|
545
|
+
const scrollTop = window.scrollY;
|
|
546
|
+
const docHeight = document.documentElement.scrollHeight;
|
|
547
|
+
const winHeight = window.innerHeight;
|
|
548
|
+
const scrollableHeight = docHeight - winHeight;
|
|
549
|
+
if (scrollableHeight <= 0) return 100;
|
|
550
|
+
return Math.min(100, Math.round(scrollTop / scrollableHeight * 100));
|
|
551
|
+
};
|
|
552
|
+
const handleScroll = () => {
|
|
553
|
+
const depth = calculateScrollDepth();
|
|
554
|
+
const elapsed = (Date.now() - startTime) / 1e3;
|
|
555
|
+
if (depth > maxDepth) {
|
|
556
|
+
maxDepth = depth;
|
|
557
|
+
currentScrollDepthRef.current = depth;
|
|
558
|
+
if (debug && depth % 25 === 0) console.log("[Analytics] Scroll milestone:", depth + "%");
|
|
559
|
+
if (depth >= 25 && milestone25 === null) milestone25 = elapsed;
|
|
560
|
+
if (depth >= 50 && milestone50 === null) milestone50 = elapsed;
|
|
561
|
+
if (depth >= 75 && milestone75 === null) milestone75 = elapsed;
|
|
562
|
+
if (depth >= 100 && milestone100 === null) milestone100 = elapsed;
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
const sendScrollData = async (useBeacon = false) => {
|
|
566
|
+
if (hasTracked || maxDepth === 0) return;
|
|
567
|
+
hasTracked = true;
|
|
568
|
+
const totalTime = (Date.now() - startTime) / 1e3;
|
|
569
|
+
const payload = JSON.stringify({
|
|
570
|
+
sessionId: sessionIdRef.current,
|
|
571
|
+
visitorId: visitorIdRef.current,
|
|
572
|
+
pagePath: pathname,
|
|
573
|
+
maxDepthPercent: maxDepth,
|
|
574
|
+
timeTo25: milestone25,
|
|
575
|
+
timeTo50: milestone50,
|
|
576
|
+
timeTo75: milestone75,
|
|
577
|
+
timeTo100: milestone100,
|
|
578
|
+
totalTimeSeconds: totalTime,
|
|
579
|
+
deviceType: getDeviceType()
|
|
580
|
+
});
|
|
581
|
+
if (useBeacon && navigator.sendBeacon) {
|
|
582
|
+
const blob = new Blob([payload], { type: "application/json" });
|
|
583
|
+
new Headers({ "x-api-key": apiKey });
|
|
584
|
+
navigator.sendBeacon(
|
|
585
|
+
`${apiUrl}/api/public/analytics/scroll-depth?key=${encodeURIComponent(apiKey)}`,
|
|
586
|
+
blob
|
|
587
|
+
);
|
|
588
|
+
if (debug) console.log("[Analytics] Scroll depth (beacon):", { maxDepth, totalTime });
|
|
589
|
+
} else {
|
|
590
|
+
try {
|
|
591
|
+
await fetch(`${apiUrl}/api/public/analytics/scroll-depth`, {
|
|
592
|
+
method: "POST",
|
|
593
|
+
headers: {
|
|
594
|
+
"Content-Type": "application/json",
|
|
595
|
+
"x-api-key": apiKey
|
|
596
|
+
},
|
|
597
|
+
body: payload,
|
|
598
|
+
keepalive: true
|
|
599
|
+
// Allows request to outlive the page
|
|
600
|
+
});
|
|
601
|
+
if (debug) console.log("[Analytics] Scroll depth:", { maxDepth, totalTime });
|
|
602
|
+
} catch (error) {
|
|
603
|
+
if (debug) console.error("[Analytics] Error tracking scroll depth:", error);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
maxDepth = 0;
|
|
608
|
+
startTime = Date.now();
|
|
609
|
+
milestone25 = null;
|
|
610
|
+
milestone50 = null;
|
|
611
|
+
milestone75 = null;
|
|
612
|
+
milestone100 = null;
|
|
613
|
+
hasTracked = false;
|
|
614
|
+
window.addEventListener("scroll", handleScroll, { passive: true });
|
|
615
|
+
const handleBeforeUnload = () => sendScrollData(true);
|
|
616
|
+
const handleVisibilityChange = () => {
|
|
617
|
+
if (document.visibilityState === "hidden") sendScrollData(true);
|
|
618
|
+
};
|
|
619
|
+
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
620
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
621
|
+
return () => {
|
|
622
|
+
window.removeEventListener("scroll", handleScroll);
|
|
623
|
+
window.removeEventListener("beforeunload", handleBeforeUnload);
|
|
624
|
+
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
625
|
+
sendScrollData(false);
|
|
626
|
+
};
|
|
627
|
+
}, [pathname, propApiUrl, propApiKey, trackScrollDepth, debug]);
|
|
628
|
+
useEffect(() => {
|
|
629
|
+
if (!trackClicks) return;
|
|
630
|
+
if (typeof window === "undefined") return;
|
|
631
|
+
const { apiUrl: globalApiUrl, apiKey: globalApiKey } = getApiConfig2();
|
|
632
|
+
const apiUrl = propApiUrl || globalApiUrl;
|
|
633
|
+
const apiKey = propApiKey || globalApiKey;
|
|
634
|
+
if (!apiKey) return;
|
|
635
|
+
const handleClick = async (e) => {
|
|
636
|
+
const target = e.target;
|
|
637
|
+
if (!target) return;
|
|
638
|
+
const docHeight = document.documentElement.scrollHeight;
|
|
639
|
+
const viewportWidth = window.innerWidth;
|
|
640
|
+
const viewportHeight = window.innerHeight;
|
|
641
|
+
const xPercent = Math.round(e.pageX / viewportWidth * 100);
|
|
642
|
+
const yPercent = Math.round(e.pageY / docHeight * 100);
|
|
643
|
+
const elementTag = target.tagName.toLowerCase();
|
|
644
|
+
const elementId = target.id || null;
|
|
645
|
+
const elementClass = target.className && typeof target.className === "string" ? target.className.split(" ").slice(0, 3).join(" ") : null;
|
|
646
|
+
const elementText = target.textContent?.slice(0, 50) || null;
|
|
647
|
+
const clickData = {
|
|
648
|
+
sessionId: sessionIdRef.current,
|
|
649
|
+
pagePath: pathname,
|
|
650
|
+
xPercent,
|
|
651
|
+
yPercent,
|
|
652
|
+
xAbsolute: e.pageX,
|
|
653
|
+
yAbsolute: e.pageY,
|
|
654
|
+
viewportWidth,
|
|
655
|
+
viewportHeight,
|
|
656
|
+
pageHeight: docHeight,
|
|
657
|
+
elementTag,
|
|
658
|
+
elementId,
|
|
659
|
+
elementClass,
|
|
660
|
+
elementText
|
|
661
|
+
};
|
|
662
|
+
if (debug) console.log("[Analytics] Click:", clickData);
|
|
663
|
+
scheduleIdleTask(async () => {
|
|
664
|
+
try {
|
|
665
|
+
await fetch(`${apiUrl}/api/public/analytics/heatmap-click`, {
|
|
666
|
+
method: "POST",
|
|
667
|
+
headers: {
|
|
668
|
+
"Content-Type": "application/json",
|
|
669
|
+
"x-api-key": apiKey
|
|
670
|
+
},
|
|
671
|
+
body: JSON.stringify(clickData)
|
|
672
|
+
});
|
|
673
|
+
} catch (error) {
|
|
674
|
+
if (debug) console.error("[Analytics] Error tracking click:", error);
|
|
675
|
+
}
|
|
676
|
+
}, 500);
|
|
677
|
+
};
|
|
678
|
+
document.addEventListener("click", handleClick, { passive: true });
|
|
679
|
+
return () => {
|
|
680
|
+
document.removeEventListener("click", handleClick);
|
|
681
|
+
};
|
|
682
|
+
}, [pathname, propApiUrl, propApiKey, trackClicks, debug]);
|
|
683
|
+
const trackEvent = useCallback((options) => {
|
|
684
|
+
const doTrack = async () => {
|
|
685
|
+
const { apiUrl: globalApiUrl, apiKey: globalApiKey } = getApiConfig2();
|
|
686
|
+
const apiUrl = propApiUrl || globalApiUrl;
|
|
687
|
+
const apiKey = propApiKey || globalApiKey;
|
|
688
|
+
if (!apiKey) {
|
|
689
|
+
if (debug) console.warn("[Analytics] No API key configured");
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
const event = {
|
|
693
|
+
sessionId: sessionIdRef.current,
|
|
694
|
+
visitorId: visitorIdRef.current,
|
|
695
|
+
eventName: options.name,
|
|
696
|
+
eventCategory: options.category,
|
|
697
|
+
eventLabel: options.label,
|
|
698
|
+
eventValue: options.value,
|
|
699
|
+
properties: options.properties,
|
|
700
|
+
pagePath: pathname
|
|
701
|
+
};
|
|
702
|
+
if (debug) {
|
|
703
|
+
console.log("[Analytics] Event:", event);
|
|
704
|
+
}
|
|
705
|
+
try {
|
|
706
|
+
const response = await fetch(`${apiUrl}/api/public/analytics/event`, {
|
|
707
|
+
method: "POST",
|
|
708
|
+
headers: {
|
|
709
|
+
"Content-Type": "application/json",
|
|
710
|
+
"x-api-key": apiKey
|
|
711
|
+
},
|
|
712
|
+
body: JSON.stringify(event)
|
|
713
|
+
});
|
|
714
|
+
if (!response.ok && debug) {
|
|
715
|
+
console.error("[Analytics] Error tracking event:", response.statusText);
|
|
716
|
+
}
|
|
717
|
+
} catch (error) {
|
|
718
|
+
if (debug) console.error("[Analytics] Error tracking event:", error);
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
scheduleIdleTask(doTrack, 1e3);
|
|
722
|
+
}, [propApiUrl, propApiKey, pathname, debug]);
|
|
723
|
+
const trackConversion = useCallback((options) => {
|
|
724
|
+
const doTrack = async () => {
|
|
725
|
+
const { apiUrl: globalApiUrl, apiKey: globalApiKey } = getApiConfig2();
|
|
726
|
+
const apiUrl = propApiUrl || globalApiUrl;
|
|
727
|
+
const apiKey = propApiKey || globalApiKey;
|
|
728
|
+
if (!apiKey) {
|
|
729
|
+
if (debug) console.warn("[Analytics] No API key configured");
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
const conversion = {
|
|
733
|
+
sessionId: sessionIdRef.current,
|
|
734
|
+
visitorId: visitorIdRef.current,
|
|
735
|
+
conversionType: options.type,
|
|
736
|
+
value: options.value,
|
|
737
|
+
currency: options.currency,
|
|
738
|
+
metadata: options.metadata,
|
|
739
|
+
pagePath: pathname,
|
|
740
|
+
referrer: document.referrer || null,
|
|
741
|
+
deviceType: getDeviceType()
|
|
742
|
+
};
|
|
743
|
+
if (debug) {
|
|
744
|
+
console.log("[Analytics] Conversion:", conversion);
|
|
745
|
+
}
|
|
746
|
+
try {
|
|
747
|
+
const response = await fetch(`${apiUrl}/api/public/analytics/conversion`, {
|
|
748
|
+
method: "POST",
|
|
749
|
+
headers: {
|
|
750
|
+
"Content-Type": "application/json",
|
|
751
|
+
"x-api-key": apiKey
|
|
752
|
+
},
|
|
753
|
+
body: JSON.stringify(conversion)
|
|
754
|
+
});
|
|
755
|
+
if (!response.ok && debug) {
|
|
756
|
+
console.error("[Analytics] Error tracking conversion:", response.statusText);
|
|
757
|
+
}
|
|
758
|
+
} catch (error) {
|
|
759
|
+
if (debug) console.error("[Analytics] Error tracking conversion:", error);
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
doTrack();
|
|
763
|
+
}, [propApiUrl, propApiKey, pathname, debug]);
|
|
764
|
+
const contextValue = useMemo(() => ({
|
|
765
|
+
trackEvent,
|
|
766
|
+
trackConversion,
|
|
767
|
+
sessionId: sessionIdRef.current,
|
|
768
|
+
visitorId: visitorIdRef.current
|
|
769
|
+
}), [trackEvent, trackConversion]);
|
|
770
|
+
return /* @__PURE__ */ jsxs(AnalyticsContext.Provider, { value: contextValue, children: [
|
|
771
|
+
trackWebVitals && /* @__PURE__ */ jsx(
|
|
772
|
+
WebVitals,
|
|
773
|
+
{
|
|
774
|
+
apiUrl: propApiUrl,
|
|
775
|
+
apiKey: propApiKey,
|
|
776
|
+
debug
|
|
777
|
+
}
|
|
778
|
+
),
|
|
779
|
+
children
|
|
780
|
+
] });
|
|
781
|
+
}
|
|
782
|
+
function useAnalytics() {
|
|
783
|
+
const context = useContext(AnalyticsContext);
|
|
784
|
+
if (!context) {
|
|
785
|
+
throw new Error("useAnalytics must be used within an AnalyticsProvider");
|
|
786
|
+
}
|
|
787
|
+
return context;
|
|
788
|
+
}
|
|
789
|
+
function useTrackEvent() {
|
|
790
|
+
const { trackEvent, trackConversion } = useAnalytics();
|
|
791
|
+
return { trackEvent, trackConversion };
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
export { AnalyticsProvider, WebVitals, useAnalytics, useTrackEvent };
|
|
795
|
+
//# sourceMappingURL=chunk-FKVJOT2F.mjs.map
|
|
796
|
+
//# sourceMappingURL=chunk-FKVJOT2F.mjs.map
|