@float.js/core 2.0.1
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/ai/index.d.ts +103 -0
- package/dist/ai/index.js +293 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/analytics/index.d.ts +193 -0
- package/dist/analytics/index.js +499 -0
- package/dist/analytics/index.js.map +1 -0
- package/dist/api/index.d.ts +172 -0
- package/dist/api/index.js +643 -0
- package/dist/api/index.js.map +1 -0
- package/dist/cli/index.js +2367 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/devtools/index.d.ts +82 -0
- package/dist/devtools/index.js +839 -0
- package/dist/devtools/index.js.map +1 -0
- package/dist/image/index.d.ts +114 -0
- package/dist/image/index.js +242 -0
- package/dist/image/index.js.map +1 -0
- package/dist/index.d.ts +400 -0
- package/dist/index.js +6511 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/index.d.ts +179 -0
- package/dist/middleware/index.js +362 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/realtime/index.d.ts +198 -0
- package/dist/realtime/index.js +555 -0
- package/dist/realtime/index.js.map +1 -0
- package/dist/router/index.d.ts +43 -0
- package/dist/router/index.js +143 -0
- package/dist/router/index.js.map +1 -0
- package/dist/server/index.d.ts +51 -0
- package/dist/server/index.js +2163 -0
- package/dist/server/index.js.map +1 -0
- package/dist/ssg/index.d.ts +178 -0
- package/dist/ssg/index.js +399 -0
- package/dist/ssg/index.js.map +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
// src/analytics/index.ts
|
|
2
|
+
var DEFAULT_CONFIG = {
|
|
3
|
+
enabled: true,
|
|
4
|
+
ignorePaths: [/^\/__float/, /^\/api\//, /\.(ico|png|jpg|css|js)$/],
|
|
5
|
+
maxBufferSize: 100,
|
|
6
|
+
flushInterval: 3e4,
|
|
7
|
+
// 30 seconds
|
|
8
|
+
hashIPs: true,
|
|
9
|
+
trackVitals: true,
|
|
10
|
+
sessionTimeout: 30,
|
|
11
|
+
// minutes
|
|
12
|
+
geoIP: false
|
|
13
|
+
};
|
|
14
|
+
function generateId() {
|
|
15
|
+
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
16
|
+
}
|
|
17
|
+
function hashString(str) {
|
|
18
|
+
let hash = 0;
|
|
19
|
+
for (let i = 0; i < str.length; i++) {
|
|
20
|
+
const char = str.charCodeAt(i);
|
|
21
|
+
hash = (hash << 5) - hash + char;
|
|
22
|
+
hash = hash & hash;
|
|
23
|
+
}
|
|
24
|
+
return Math.abs(hash).toString(36);
|
|
25
|
+
}
|
|
26
|
+
function parseUserAgent(ua) {
|
|
27
|
+
if (!ua) {
|
|
28
|
+
return { device: "unknown", browser: "Unknown", os: "Unknown" };
|
|
29
|
+
}
|
|
30
|
+
let device = "desktop";
|
|
31
|
+
if (/Mobile|Android.*Mobile|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua)) {
|
|
32
|
+
device = "mobile";
|
|
33
|
+
} else if (/iPad|Android(?!.*Mobile)|Tablet/i.test(ua)) {
|
|
34
|
+
device = "tablet";
|
|
35
|
+
}
|
|
36
|
+
let browser = "Unknown";
|
|
37
|
+
if (/Firefox/i.test(ua)) browser = "Firefox";
|
|
38
|
+
else if (/Edg/i.test(ua)) browser = "Edge";
|
|
39
|
+
else if (/Chrome/i.test(ua)) browser = "Chrome";
|
|
40
|
+
else if (/Safari/i.test(ua)) browser = "Safari";
|
|
41
|
+
else if (/Opera|OPR/i.test(ua)) browser = "Opera";
|
|
42
|
+
let os = "Unknown";
|
|
43
|
+
if (/Windows/i.test(ua)) os = "Windows";
|
|
44
|
+
else if (/Mac OS X/i.test(ua)) os = "macOS";
|
|
45
|
+
else if (/Linux/i.test(ua)) os = "Linux";
|
|
46
|
+
else if (/Android/i.test(ua)) os = "Android";
|
|
47
|
+
else if (/iOS|iPhone|iPad/i.test(ua)) os = "iOS";
|
|
48
|
+
return { device, browser, os };
|
|
49
|
+
}
|
|
50
|
+
function percentile(arr, p) {
|
|
51
|
+
if (arr.length === 0) return 0;
|
|
52
|
+
const sorted = [...arr].sort((a, b) => a - b);
|
|
53
|
+
const index = Math.ceil(p / 100 * sorted.length) - 1;
|
|
54
|
+
return sorted[Math.max(0, index)];
|
|
55
|
+
}
|
|
56
|
+
var AnalyticsEngine = class {
|
|
57
|
+
config;
|
|
58
|
+
buffer = {
|
|
59
|
+
pageviews: [],
|
|
60
|
+
vitals: [],
|
|
61
|
+
events: []
|
|
62
|
+
};
|
|
63
|
+
flushTimer = null;
|
|
64
|
+
sessions = /* @__PURE__ */ new Map();
|
|
65
|
+
allData = {
|
|
66
|
+
pageviews: [],
|
|
67
|
+
vitals: [],
|
|
68
|
+
events: []
|
|
69
|
+
};
|
|
70
|
+
constructor(config = {}) {
|
|
71
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
72
|
+
this.startFlushTimer();
|
|
73
|
+
}
|
|
74
|
+
startFlushTimer() {
|
|
75
|
+
if (this.flushTimer) clearInterval(this.flushTimer);
|
|
76
|
+
this.flushTimer = setInterval(() => {
|
|
77
|
+
this.flush();
|
|
78
|
+
}, this.config.flushInterval);
|
|
79
|
+
}
|
|
80
|
+
shouldIgnore(pathname) {
|
|
81
|
+
return this.config.ignorePaths.some((pattern) => {
|
|
82
|
+
if (typeof pattern === "string") {
|
|
83
|
+
return pathname.startsWith(pattern);
|
|
84
|
+
}
|
|
85
|
+
return pattern.test(pathname);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
getOrCreateSession(ip) {
|
|
89
|
+
const sessionKey = this.config.hashIPs ? hashString(ip) : ip;
|
|
90
|
+
const existing = this.sessions.get(sessionKey);
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
const timeout = this.config.sessionTimeout * 60 * 1e3;
|
|
93
|
+
if (existing && now - existing.lastActivity < timeout) {
|
|
94
|
+
existing.lastActivity = now;
|
|
95
|
+
existing.views++;
|
|
96
|
+
return sessionKey;
|
|
97
|
+
}
|
|
98
|
+
this.sessions.set(sessionKey, { lastActivity: now, views: 1 });
|
|
99
|
+
return sessionKey;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Track a page view
|
|
103
|
+
*/
|
|
104
|
+
trackPageview(req, options = {}) {
|
|
105
|
+
if (!this.config.enabled) return null;
|
|
106
|
+
const url = new URL(req.url);
|
|
107
|
+
if (this.shouldIgnore(url.pathname)) return null;
|
|
108
|
+
const ip = req.headers.get("x-forwarded-for")?.split(",")[0] || req.headers.get("x-real-ip") || "unknown";
|
|
109
|
+
const userAgent = req.headers.get("user-agent") || void 0;
|
|
110
|
+
const { device, browser, os } = parseUserAgent(userAgent);
|
|
111
|
+
const sessionId = this.getOrCreateSession(ip);
|
|
112
|
+
const pageview = {
|
|
113
|
+
id: generateId(),
|
|
114
|
+
timestamp: Date.now(),
|
|
115
|
+
pathname: url.pathname,
|
|
116
|
+
referrer: req.headers.get("referer") || void 0,
|
|
117
|
+
userAgent: userAgent?.substring(0, 200),
|
|
118
|
+
// Truncate for storage
|
|
119
|
+
country: options.country,
|
|
120
|
+
device,
|
|
121
|
+
browser,
|
|
122
|
+
os,
|
|
123
|
+
sessionId
|
|
124
|
+
};
|
|
125
|
+
this.buffer.pageviews.push(pageview);
|
|
126
|
+
this.checkBufferSize();
|
|
127
|
+
return pageview;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Track Web Vitals
|
|
131
|
+
*/
|
|
132
|
+
trackVitals(pathname, metrics) {
|
|
133
|
+
if (!this.config.enabled || !this.config.trackVitals) return null;
|
|
134
|
+
const vitals = {
|
|
135
|
+
id: generateId(),
|
|
136
|
+
timestamp: Date.now(),
|
|
137
|
+
pathname,
|
|
138
|
+
metrics
|
|
139
|
+
};
|
|
140
|
+
this.buffer.vitals.push(vitals);
|
|
141
|
+
this.checkBufferSize();
|
|
142
|
+
return vitals;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Track custom event
|
|
146
|
+
*/
|
|
147
|
+
trackEvent(name, properties, req) {
|
|
148
|
+
if (!this.config.enabled) return null;
|
|
149
|
+
let pathname = "/";
|
|
150
|
+
let sessionId = generateId();
|
|
151
|
+
if (req) {
|
|
152
|
+
const url = new URL(req.url);
|
|
153
|
+
pathname = url.pathname;
|
|
154
|
+
const ip = req.headers.get("x-forwarded-for")?.split(",")[0] || req.headers.get("x-real-ip") || "unknown";
|
|
155
|
+
sessionId = this.getOrCreateSession(ip);
|
|
156
|
+
}
|
|
157
|
+
const event = {
|
|
158
|
+
id: generateId(),
|
|
159
|
+
timestamp: Date.now(),
|
|
160
|
+
name,
|
|
161
|
+
pathname,
|
|
162
|
+
properties,
|
|
163
|
+
sessionId
|
|
164
|
+
};
|
|
165
|
+
this.buffer.events.push(event);
|
|
166
|
+
this.checkBufferSize();
|
|
167
|
+
return event;
|
|
168
|
+
}
|
|
169
|
+
checkBufferSize() {
|
|
170
|
+
const totalSize = this.buffer.pageviews.length + this.buffer.vitals.length + this.buffer.events.length;
|
|
171
|
+
if (totalSize >= this.config.maxBufferSize) {
|
|
172
|
+
this.flush();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Flush buffer to storage/handler
|
|
177
|
+
*/
|
|
178
|
+
async flush() {
|
|
179
|
+
if (this.buffer.pageviews.length === 0 && this.buffer.vitals.length === 0 && this.buffer.events.length === 0) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const dataToFlush = { ...this.buffer };
|
|
183
|
+
this.buffer = { pageviews: [], vitals: [], events: [] };
|
|
184
|
+
this.allData.pageviews.push(...dataToFlush.pageviews);
|
|
185
|
+
this.allData.vitals.push(...dataToFlush.vitals);
|
|
186
|
+
this.allData.events.push(...dataToFlush.events);
|
|
187
|
+
if (this.allData.pageviews.length > 1e4) {
|
|
188
|
+
this.allData.pageviews = this.allData.pageviews.slice(-1e4);
|
|
189
|
+
}
|
|
190
|
+
if (this.allData.vitals.length > 1e4) {
|
|
191
|
+
this.allData.vitals = this.allData.vitals.slice(-1e4);
|
|
192
|
+
}
|
|
193
|
+
if (this.allData.events.length > 1e4) {
|
|
194
|
+
this.allData.events = this.allData.events.slice(-1e4);
|
|
195
|
+
}
|
|
196
|
+
if (this.config.onFlush) {
|
|
197
|
+
await this.config.onFlush(dataToFlush);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Get analytics summary
|
|
202
|
+
*/
|
|
203
|
+
getSummary(startDate, endDate) {
|
|
204
|
+
const start = startDate?.getTime() || Date.now() - 7 * 24 * 60 * 60 * 1e3;
|
|
205
|
+
const end = endDate?.getTime() || Date.now();
|
|
206
|
+
const pageviews = this.allData.pageviews.filter(
|
|
207
|
+
(pv) => pv.timestamp >= start && pv.timestamp <= end
|
|
208
|
+
);
|
|
209
|
+
const vitals = this.allData.vitals.filter(
|
|
210
|
+
(v) => v.timestamp >= start && v.timestamp <= end
|
|
211
|
+
);
|
|
212
|
+
const events = this.allData.events.filter(
|
|
213
|
+
(e) => e.timestamp >= start && e.timestamp <= end
|
|
214
|
+
);
|
|
215
|
+
const uniqueSessions = new Set(pageviews.map((pv) => pv.sessionId));
|
|
216
|
+
const byPath = {};
|
|
217
|
+
const byReferrer = {};
|
|
218
|
+
const byDevice = {};
|
|
219
|
+
const byBrowser = {};
|
|
220
|
+
const byCountry = {};
|
|
221
|
+
for (const pv of pageviews) {
|
|
222
|
+
byPath[pv.pathname] = (byPath[pv.pathname] || 0) + 1;
|
|
223
|
+
if (pv.referrer) {
|
|
224
|
+
try {
|
|
225
|
+
const ref = new URL(pv.referrer).hostname;
|
|
226
|
+
byReferrer[ref] = (byReferrer[ref] || 0) + 1;
|
|
227
|
+
} catch {
|
|
228
|
+
byReferrer["direct"] = (byReferrer["direct"] || 0) + 1;
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
byReferrer["direct"] = (byReferrer["direct"] || 0) + 1;
|
|
232
|
+
}
|
|
233
|
+
byDevice[pv.device] = (byDevice[pv.device] || 0) + 1;
|
|
234
|
+
byBrowser[pv.browser] = (byBrowser[pv.browser] || 0) + 1;
|
|
235
|
+
if (pv.country) {
|
|
236
|
+
byCountry[pv.country] = (byCountry[pv.country] || 0) + 1;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const fcpValues = vitals.filter((v) => v.metrics.FCP).map((v) => v.metrics.FCP);
|
|
240
|
+
const lcpValues = vitals.filter((v) => v.metrics.LCP).map((v) => v.metrics.LCP);
|
|
241
|
+
const fidValues = vitals.filter((v) => v.metrics.FID).map((v) => v.metrics.FID);
|
|
242
|
+
const clsValues = vitals.filter((v) => v.metrics.CLS).map((v) => v.metrics.CLS);
|
|
243
|
+
const ttfbValues = vitals.filter((v) => v.metrics.TTFB).map((v) => v.metrics.TTFB);
|
|
244
|
+
const avg = (arr) => arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : 0;
|
|
245
|
+
const sessionViews = /* @__PURE__ */ new Map();
|
|
246
|
+
for (const pv of pageviews) {
|
|
247
|
+
sessionViews.set(pv.sessionId, (sessionViews.get(pv.sessionId) || 0) + 1);
|
|
248
|
+
}
|
|
249
|
+
const bounces = Array.from(sessionViews.values()).filter((v) => v === 1).length;
|
|
250
|
+
const bounceRate = sessionViews.size > 0 ? bounces / sessionViews.size * 100 : 0;
|
|
251
|
+
const byEventName = {};
|
|
252
|
+
for (const e of events) {
|
|
253
|
+
byEventName[e.name] = (byEventName[e.name] || 0) + 1;
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
period: { start: new Date(start), end: new Date(end) },
|
|
257
|
+
pageviews: {
|
|
258
|
+
total: pageviews.length,
|
|
259
|
+
unique: uniqueSessions.size,
|
|
260
|
+
byPath,
|
|
261
|
+
byReferrer,
|
|
262
|
+
byDevice,
|
|
263
|
+
byBrowser,
|
|
264
|
+
byCountry
|
|
265
|
+
},
|
|
266
|
+
vitals: {
|
|
267
|
+
avgFCP: Math.round(avg(fcpValues)),
|
|
268
|
+
avgLCP: Math.round(avg(lcpValues)),
|
|
269
|
+
avgFID: Math.round(avg(fidValues)),
|
|
270
|
+
avgCLS: Number(avg(clsValues).toFixed(3)),
|
|
271
|
+
avgTTFB: Math.round(avg(ttfbValues)),
|
|
272
|
+
p75LCP: Math.round(percentile(lcpValues, 75)),
|
|
273
|
+
p75FID: Math.round(percentile(fidValues, 75)),
|
|
274
|
+
p75CLS: Number(percentile(clsValues, 75).toFixed(3))
|
|
275
|
+
},
|
|
276
|
+
events: {
|
|
277
|
+
total: events.length,
|
|
278
|
+
byName: byEventName
|
|
279
|
+
},
|
|
280
|
+
sessions: {
|
|
281
|
+
total: uniqueSessions.size,
|
|
282
|
+
avgDuration: 0,
|
|
283
|
+
// Would need page durations
|
|
284
|
+
bounceRate: Number(bounceRate.toFixed(1))
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Get real-time stats (last 5 minutes)
|
|
290
|
+
*/
|
|
291
|
+
getRealtime() {
|
|
292
|
+
const fiveMinutesAgo = Date.now() - 5 * 60 * 1e3;
|
|
293
|
+
const recentPageviews = this.allData.pageviews.filter(
|
|
294
|
+
(pv) => pv.timestamp >= fiveMinutesAgo
|
|
295
|
+
);
|
|
296
|
+
const activeSessions = new Set(recentPageviews.map((pv) => pv.sessionId));
|
|
297
|
+
const pathCounts = {};
|
|
298
|
+
for (const pv of recentPageviews) {
|
|
299
|
+
pathCounts[pv.pathname] = (pathCounts[pv.pathname] || 0) + 1;
|
|
300
|
+
}
|
|
301
|
+
const topPages = Object.entries(pathCounts).map(([path, count]) => ({ path, count })).sort((a, b) => b.count - a.count).slice(0, 10);
|
|
302
|
+
return {
|
|
303
|
+
activeUsers: activeSessions.size,
|
|
304
|
+
pageviews: recentPageviews.length,
|
|
305
|
+
topPages
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Export data as JSON
|
|
310
|
+
*/
|
|
311
|
+
exportData() {
|
|
312
|
+
return { ...this.allData };
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Clear all analytics data
|
|
316
|
+
*/
|
|
317
|
+
clearData() {
|
|
318
|
+
this.buffer = { pageviews: [], vitals: [], events: [] };
|
|
319
|
+
this.allData = { pageviews: [], vitals: [], events: [] };
|
|
320
|
+
this.sessions.clear();
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Stop the analytics engine
|
|
324
|
+
*/
|
|
325
|
+
stop() {
|
|
326
|
+
if (this.flushTimer) {
|
|
327
|
+
clearInterval(this.flushTimer);
|
|
328
|
+
this.flushTimer = null;
|
|
329
|
+
}
|
|
330
|
+
this.flush();
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
var analyticsEngine = null;
|
|
334
|
+
function getAnalytics(config) {
|
|
335
|
+
if (!analyticsEngine) {
|
|
336
|
+
analyticsEngine = new AnalyticsEngine(config);
|
|
337
|
+
}
|
|
338
|
+
return analyticsEngine;
|
|
339
|
+
}
|
|
340
|
+
function configureAnalytics(config) {
|
|
341
|
+
analyticsEngine = new AnalyticsEngine(config);
|
|
342
|
+
return analyticsEngine;
|
|
343
|
+
}
|
|
344
|
+
function createAnalyticsMiddleware(options = {}) {
|
|
345
|
+
const engine = getAnalytics(options.config);
|
|
346
|
+
return async (req, next) => {
|
|
347
|
+
engine.trackPageview(req, {
|
|
348
|
+
country: options.getCountry?.(req)
|
|
349
|
+
});
|
|
350
|
+
const response = await next();
|
|
351
|
+
return response;
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
function createAnalyticsHandler() {
|
|
355
|
+
const engine = getAnalytics();
|
|
356
|
+
return async (req) => {
|
|
357
|
+
const url = new URL(req.url);
|
|
358
|
+
const action = url.searchParams.get("action") || "summary";
|
|
359
|
+
switch (action) {
|
|
360
|
+
case "summary": {
|
|
361
|
+
const startParam = url.searchParams.get("start");
|
|
362
|
+
const endParam = url.searchParams.get("end");
|
|
363
|
+
const summary = engine.getSummary(
|
|
364
|
+
startParam ? new Date(startParam) : void 0,
|
|
365
|
+
endParam ? new Date(endParam) : void 0
|
|
366
|
+
);
|
|
367
|
+
return new Response(JSON.stringify(summary), {
|
|
368
|
+
headers: { "Content-Type": "application/json" }
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
case "realtime": {
|
|
372
|
+
const realtime = engine.getRealtime();
|
|
373
|
+
return new Response(JSON.stringify(realtime), {
|
|
374
|
+
headers: { "Content-Type": "application/json" }
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
case "vitals": {
|
|
378
|
+
if (req.method !== "POST") {
|
|
379
|
+
return new Response("Method not allowed", { status: 405 });
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
const body = await req.json();
|
|
383
|
+
engine.trackVitals(body.pathname, body.metrics);
|
|
384
|
+
return new Response(JSON.stringify({ success: true }), {
|
|
385
|
+
headers: { "Content-Type": "application/json" }
|
|
386
|
+
});
|
|
387
|
+
} catch {
|
|
388
|
+
return new Response("Invalid body", { status: 400 });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
case "event": {
|
|
392
|
+
if (req.method !== "POST") {
|
|
393
|
+
return new Response("Method not allowed", { status: 405 });
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
const body = await req.json();
|
|
397
|
+
engine.trackEvent(body.name, body.properties, req);
|
|
398
|
+
return new Response(JSON.stringify({ success: true }), {
|
|
399
|
+
headers: { "Content-Type": "application/json" }
|
|
400
|
+
});
|
|
401
|
+
} catch {
|
|
402
|
+
return new Response("Invalid body", { status: 400 });
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
case "export": {
|
|
406
|
+
const data = engine.exportData();
|
|
407
|
+
return new Response(JSON.stringify(data), {
|
|
408
|
+
headers: {
|
|
409
|
+
"Content-Type": "application/json",
|
|
410
|
+
"Content-Disposition": 'attachment; filename="analytics-export.json"'
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
default:
|
|
415
|
+
return new Response("Unknown action", { status: 400 });
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
var analyticsClientScript = `
|
|
420
|
+
<script>
|
|
421
|
+
(function() {
|
|
422
|
+
// Simple Web Vitals reporter
|
|
423
|
+
const endpoint = '/__float/analytics?action=vitals';
|
|
424
|
+
|
|
425
|
+
function sendVitals(metrics) {
|
|
426
|
+
const body = JSON.stringify({
|
|
427
|
+
pathname: window.location.pathname,
|
|
428
|
+
metrics: metrics
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
if (navigator.sendBeacon) {
|
|
432
|
+
navigator.sendBeacon(endpoint, body);
|
|
433
|
+
} else {
|
|
434
|
+
fetch(endpoint, { method: 'POST', body, keepalive: true });
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Observe LCP
|
|
439
|
+
if ('PerformanceObserver' in window) {
|
|
440
|
+
const vitals = {};
|
|
441
|
+
|
|
442
|
+
// LCP
|
|
443
|
+
new PerformanceObserver((list) => {
|
|
444
|
+
const entries = list.getEntries();
|
|
445
|
+
const lastEntry = entries[entries.length - 1];
|
|
446
|
+
vitals.LCP = Math.round(lastEntry.startTime);
|
|
447
|
+
}).observe({ type: 'largest-contentful-paint', buffered: true });
|
|
448
|
+
|
|
449
|
+
// FCP
|
|
450
|
+
new PerformanceObserver((list) => {
|
|
451
|
+
const entries = list.getEntries();
|
|
452
|
+
vitals.FCP = Math.round(entries[0].startTime);
|
|
453
|
+
}).observe({ type: 'paint', buffered: true });
|
|
454
|
+
|
|
455
|
+
// CLS
|
|
456
|
+
let clsValue = 0;
|
|
457
|
+
new PerformanceObserver((list) => {
|
|
458
|
+
for (const entry of list.getEntries()) {
|
|
459
|
+
if (!entry.hadRecentInput) {
|
|
460
|
+
clsValue += entry.value;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
vitals.CLS = clsValue;
|
|
464
|
+
}).observe({ type: 'layout-shift', buffered: true });
|
|
465
|
+
|
|
466
|
+
// Send on page hide
|
|
467
|
+
document.addEventListener('visibilitychange', () => {
|
|
468
|
+
if (document.visibilityState === 'hidden') {
|
|
469
|
+
sendVitals(vitals);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
})();
|
|
474
|
+
</script>
|
|
475
|
+
`;
|
|
476
|
+
var analytics = {
|
|
477
|
+
engine: getAnalytics,
|
|
478
|
+
configure: configureAnalytics,
|
|
479
|
+
createMiddleware: createAnalyticsMiddleware,
|
|
480
|
+
createHandler: createAnalyticsHandler,
|
|
481
|
+
clientScript: analyticsClientScript,
|
|
482
|
+
track: {
|
|
483
|
+
pageview: (req, options) => getAnalytics().trackPageview(req, options),
|
|
484
|
+
event: (name, properties, req) => getAnalytics().trackEvent(name, properties, req),
|
|
485
|
+
vitals: (pathname, metrics) => getAnalytics().trackVitals(pathname, metrics)
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
var analytics_default = analytics;
|
|
489
|
+
export {
|
|
490
|
+
AnalyticsEngine,
|
|
491
|
+
analytics,
|
|
492
|
+
analyticsClientScript,
|
|
493
|
+
configureAnalytics,
|
|
494
|
+
createAnalyticsHandler,
|
|
495
|
+
createAnalyticsMiddleware,
|
|
496
|
+
analytics_default as default,
|
|
497
|
+
getAnalytics
|
|
498
|
+
};
|
|
499
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/analytics/index.ts"],"sourcesContent":["/**\n * Float.js - Built-in Analytics\n * \n * Zero-config privacy-focused analytics built into the framework.\n * No external dependencies, no cookies, GDPR-compliant by default.\n */\n\n// ============================================================================\n// TYPES\n// ============================================================================\n\nexport interface PageView {\n id: string;\n timestamp: number;\n pathname: string;\n referrer?: string;\n userAgent?: string;\n country?: string;\n device: 'desktop' | 'mobile' | 'tablet' | 'unknown';\n browser: string;\n os: string;\n sessionId: string;\n duration?: number;\n}\n\nexport interface WebVitals {\n id: string;\n timestamp: number;\n pathname: string;\n metrics: {\n FCP?: number; // First Contentful Paint\n LCP?: number; // Largest Contentful Paint\n FID?: number; // First Input Delay\n CLS?: number; // Cumulative Layout Shift\n TTFB?: number; // Time to First Byte\n INP?: number; // Interaction to Next Paint\n };\n}\n\nexport interface CustomEvent {\n id: string;\n timestamp: number;\n name: string;\n pathname: string;\n properties?: Record<string, string | number | boolean>;\n sessionId: string;\n}\n\nexport interface AnalyticsData {\n pageviews: PageView[];\n vitals: WebVitals[];\n events: CustomEvent[];\n}\n\nexport interface AnalyticsConfig {\n /** Enable/disable analytics */\n enabled: boolean;\n /** Ignore paths (regex patterns) */\n ignorePaths: (string | RegExp)[];\n /** Max events in memory before flush */\n maxBufferSize: number;\n /** Flush interval in ms */\n flushInterval: number;\n /** Custom event handler */\n onFlush?: (data: AnalyticsData) => Promise<void>;\n /** Hash IP addresses for privacy */\n hashIPs: boolean;\n /** Track web vitals */\n trackVitals: boolean;\n /** Session timeout in minutes */\n sessionTimeout: number;\n /** GeoIP lookup */\n geoIP: boolean;\n}\n\nexport interface AnalyticsSummary {\n period: { start: Date; end: Date };\n pageviews: {\n total: number;\n unique: number;\n byPath: Record<string, number>;\n byReferrer: Record<string, number>;\n byDevice: Record<string, number>;\n byBrowser: Record<string, number>;\n byCountry: Record<string, number>;\n };\n vitals: {\n avgFCP: number;\n avgLCP: number;\n avgFID: number;\n avgCLS: number;\n avgTTFB: number;\n p75LCP: number;\n p75FID: number;\n p75CLS: number;\n };\n events: {\n total: number;\n byName: Record<string, number>;\n };\n sessions: {\n total: number;\n avgDuration: number;\n bounceRate: number;\n };\n}\n\n// ============================================================================\n// DEFAULT CONFIG\n// ============================================================================\n\nconst DEFAULT_CONFIG: AnalyticsConfig = {\n enabled: true,\n ignorePaths: [/^\\/__float/, /^\\/api\\//, /\\.(ico|png|jpg|css|js)$/],\n maxBufferSize: 100,\n flushInterval: 30000, // 30 seconds\n hashIPs: true,\n trackVitals: true,\n sessionTimeout: 30, // minutes\n geoIP: false\n};\n\n// ============================================================================\n// UTILITIES\n// ============================================================================\n\nfunction generateId(): string {\n return Math.random().toString(36).substring(2, 15) + \n Math.random().toString(36).substring(2, 15);\n}\n\nfunction hashString(str: string): string {\n let hash = 0;\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i);\n hash = ((hash << 5) - hash) + char;\n hash = hash & hash;\n }\n return Math.abs(hash).toString(36);\n}\n\nfunction parseUserAgent(ua?: string): { device: PageView['device']; browser: string; os: string } {\n if (!ua) {\n return { device: 'unknown', browser: 'Unknown', os: 'Unknown' };\n }\n\n // Device detection\n let device: PageView['device'] = 'desktop';\n if (/Mobile|Android.*Mobile|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua)) {\n device = 'mobile';\n } else if (/iPad|Android(?!.*Mobile)|Tablet/i.test(ua)) {\n device = 'tablet';\n }\n\n // Browser detection\n let browser = 'Unknown';\n if (/Firefox/i.test(ua)) browser = 'Firefox';\n else if (/Edg/i.test(ua)) browser = 'Edge';\n else if (/Chrome/i.test(ua)) browser = 'Chrome';\n else if (/Safari/i.test(ua)) browser = 'Safari';\n else if (/Opera|OPR/i.test(ua)) browser = 'Opera';\n\n // OS detection\n let os = 'Unknown';\n if (/Windows/i.test(ua)) os = 'Windows';\n else if (/Mac OS X/i.test(ua)) os = 'macOS';\n else if (/Linux/i.test(ua)) os = 'Linux';\n else if (/Android/i.test(ua)) os = 'Android';\n else if (/iOS|iPhone|iPad/i.test(ua)) os = 'iOS';\n\n return { device, browser, os };\n}\n\nfunction percentile(arr: number[], p: number): number {\n if (arr.length === 0) return 0;\n const sorted = [...arr].sort((a, b) => a - b);\n const index = Math.ceil((p / 100) * sorted.length) - 1;\n return sorted[Math.max(0, index)];\n}\n\n// ============================================================================\n// ANALYTICS ENGINE\n// ============================================================================\n\nexport class AnalyticsEngine {\n private config: AnalyticsConfig;\n private buffer: AnalyticsData = {\n pageviews: [],\n vitals: [],\n events: []\n };\n private flushTimer: NodeJS.Timeout | null = null;\n private sessions: Map<string, { lastActivity: number; views: number }> = new Map();\n private allData: AnalyticsData = {\n pageviews: [],\n vitals: [],\n events: []\n };\n\n constructor(config: Partial<AnalyticsConfig> = {}) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n this.startFlushTimer();\n }\n\n private startFlushTimer(): void {\n if (this.flushTimer) clearInterval(this.flushTimer);\n \n this.flushTimer = setInterval(() => {\n this.flush();\n }, this.config.flushInterval);\n }\n\n private shouldIgnore(pathname: string): boolean {\n return this.config.ignorePaths.some(pattern => {\n if (typeof pattern === 'string') {\n return pathname.startsWith(pattern);\n }\n return pattern.test(pathname);\n });\n }\n\n private getOrCreateSession(ip: string): string {\n const sessionKey = this.config.hashIPs ? hashString(ip) : ip;\n const existing = this.sessions.get(sessionKey);\n const now = Date.now();\n const timeout = this.config.sessionTimeout * 60 * 1000;\n\n if (existing && (now - existing.lastActivity) < timeout) {\n existing.lastActivity = now;\n existing.views++;\n return sessionKey;\n }\n\n this.sessions.set(sessionKey, { lastActivity: now, views: 1 });\n return sessionKey;\n }\n\n /**\n * Track a page view\n */\n trackPageview(req: Request, options: { country?: string } = {}): PageView | null {\n if (!this.config.enabled) return null;\n\n const url = new URL(req.url);\n \n if (this.shouldIgnore(url.pathname)) return null;\n\n const ip = req.headers.get('x-forwarded-for')?.split(',')[0] || \n req.headers.get('x-real-ip') || \n 'unknown';\n const userAgent = req.headers.get('user-agent') || undefined;\n const { device, browser, os } = parseUserAgent(userAgent);\n const sessionId = this.getOrCreateSession(ip);\n\n const pageview: PageView = {\n id: generateId(),\n timestamp: Date.now(),\n pathname: url.pathname,\n referrer: req.headers.get('referer') || undefined,\n userAgent: userAgent?.substring(0, 200), // Truncate for storage\n country: options.country,\n device,\n browser,\n os,\n sessionId\n };\n\n this.buffer.pageviews.push(pageview);\n this.checkBufferSize();\n\n return pageview;\n }\n\n /**\n * Track Web Vitals\n */\n trackVitals(pathname: string, metrics: WebVitals['metrics']): WebVitals | null {\n if (!this.config.enabled || !this.config.trackVitals) return null;\n\n const vitals: WebVitals = {\n id: generateId(),\n timestamp: Date.now(),\n pathname,\n metrics\n };\n\n this.buffer.vitals.push(vitals);\n this.checkBufferSize();\n\n return vitals;\n }\n\n /**\n * Track custom event\n */\n trackEvent(\n name: string, \n properties?: Record<string, string | number | boolean>,\n req?: Request\n ): CustomEvent | null {\n if (!this.config.enabled) return null;\n\n let pathname = '/';\n let sessionId = generateId();\n\n if (req) {\n const url = new URL(req.url);\n pathname = url.pathname;\n \n const ip = req.headers.get('x-forwarded-for')?.split(',')[0] || \n req.headers.get('x-real-ip') || \n 'unknown';\n sessionId = this.getOrCreateSession(ip);\n }\n\n const event: CustomEvent = {\n id: generateId(),\n timestamp: Date.now(),\n name,\n pathname,\n properties,\n sessionId\n };\n\n this.buffer.events.push(event);\n this.checkBufferSize();\n\n return event;\n }\n\n private checkBufferSize(): void {\n const totalSize = this.buffer.pageviews.length + \n this.buffer.vitals.length + \n this.buffer.events.length;\n \n if (totalSize >= this.config.maxBufferSize) {\n this.flush();\n }\n }\n\n /**\n * Flush buffer to storage/handler\n */\n async flush(): Promise<void> {\n if (this.buffer.pageviews.length === 0 && \n this.buffer.vitals.length === 0 && \n this.buffer.events.length === 0) {\n return;\n }\n\n const dataToFlush = { ...this.buffer };\n \n // Reset buffer\n this.buffer = { pageviews: [], vitals: [], events: [] };\n\n // Add to all data for summary\n this.allData.pageviews.push(...dataToFlush.pageviews);\n this.allData.vitals.push(...dataToFlush.vitals);\n this.allData.events.push(...dataToFlush.events);\n\n // Limit stored data (keep last 10000 of each)\n if (this.allData.pageviews.length > 10000) {\n this.allData.pageviews = this.allData.pageviews.slice(-10000);\n }\n if (this.allData.vitals.length > 10000) {\n this.allData.vitals = this.allData.vitals.slice(-10000);\n }\n if (this.allData.events.length > 10000) {\n this.allData.events = this.allData.events.slice(-10000);\n }\n\n // Call custom handler if provided\n if (this.config.onFlush) {\n await this.config.onFlush(dataToFlush);\n }\n }\n\n /**\n * Get analytics summary\n */\n getSummary(startDate?: Date, endDate?: Date): AnalyticsSummary {\n const start = startDate?.getTime() || Date.now() - (7 * 24 * 60 * 60 * 1000); // Last 7 days\n const end = endDate?.getTime() || Date.now();\n\n // Filter by date range\n const pageviews = this.allData.pageviews.filter(\n pv => pv.timestamp >= start && pv.timestamp <= end\n );\n const vitals = this.allData.vitals.filter(\n v => v.timestamp >= start && v.timestamp <= end\n );\n const events = this.allData.events.filter(\n e => e.timestamp >= start && e.timestamp <= end\n );\n\n // Calculate pageview stats\n const uniqueSessions = new Set(pageviews.map(pv => pv.sessionId));\n const byPath: Record<string, number> = {};\n const byReferrer: Record<string, number> = {};\n const byDevice: Record<string, number> = {};\n const byBrowser: Record<string, number> = {};\n const byCountry: Record<string, number> = {};\n\n for (const pv of pageviews) {\n byPath[pv.pathname] = (byPath[pv.pathname] || 0) + 1;\n if (pv.referrer) {\n try {\n const ref = new URL(pv.referrer).hostname;\n byReferrer[ref] = (byReferrer[ref] || 0) + 1;\n } catch {\n byReferrer['direct'] = (byReferrer['direct'] || 0) + 1;\n }\n } else {\n byReferrer['direct'] = (byReferrer['direct'] || 0) + 1;\n }\n byDevice[pv.device] = (byDevice[pv.device] || 0) + 1;\n byBrowser[pv.browser] = (byBrowser[pv.browser] || 0) + 1;\n if (pv.country) {\n byCountry[pv.country] = (byCountry[pv.country] || 0) + 1;\n }\n }\n\n // Calculate vitals averages\n const fcpValues = vitals.filter(v => v.metrics.FCP).map(v => v.metrics.FCP!);\n const lcpValues = vitals.filter(v => v.metrics.LCP).map(v => v.metrics.LCP!);\n const fidValues = vitals.filter(v => v.metrics.FID).map(v => v.metrics.FID!);\n const clsValues = vitals.filter(v => v.metrics.CLS).map(v => v.metrics.CLS!);\n const ttfbValues = vitals.filter(v => v.metrics.TTFB).map(v => v.metrics.TTFB!);\n\n const avg = (arr: number[]) => arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : 0;\n\n // Calculate session stats\n const sessionViews = new Map<string, number>();\n for (const pv of pageviews) {\n sessionViews.set(pv.sessionId, (sessionViews.get(pv.sessionId) || 0) + 1);\n }\n const bounces = Array.from(sessionViews.values()).filter(v => v === 1).length;\n const bounceRate = sessionViews.size > 0 ? (bounces / sessionViews.size) * 100 : 0;\n\n // Calculate event stats\n const byEventName: Record<string, number> = {};\n for (const e of events) {\n byEventName[e.name] = (byEventName[e.name] || 0) + 1;\n }\n\n return {\n period: { start: new Date(start), end: new Date(end) },\n pageviews: {\n total: pageviews.length,\n unique: uniqueSessions.size,\n byPath,\n byReferrer,\n byDevice,\n byBrowser,\n byCountry\n },\n vitals: {\n avgFCP: Math.round(avg(fcpValues)),\n avgLCP: Math.round(avg(lcpValues)),\n avgFID: Math.round(avg(fidValues)),\n avgCLS: Number(avg(clsValues).toFixed(3)),\n avgTTFB: Math.round(avg(ttfbValues)),\n p75LCP: Math.round(percentile(lcpValues, 75)),\n p75FID: Math.round(percentile(fidValues, 75)),\n p75CLS: Number(percentile(clsValues, 75).toFixed(3))\n },\n events: {\n total: events.length,\n byName: byEventName\n },\n sessions: {\n total: uniqueSessions.size,\n avgDuration: 0, // Would need page durations\n bounceRate: Number(bounceRate.toFixed(1))\n }\n };\n }\n\n /**\n * Get real-time stats (last 5 minutes)\n */\n getRealtime(): {\n activeUsers: number;\n pageviews: number;\n topPages: Array<{ path: string; count: number }>;\n } {\n const fiveMinutesAgo = Date.now() - (5 * 60 * 1000);\n \n const recentPageviews = this.allData.pageviews.filter(\n pv => pv.timestamp >= fiveMinutesAgo\n );\n\n const activeSessions = new Set(recentPageviews.map(pv => pv.sessionId));\n \n const pathCounts: Record<string, number> = {};\n for (const pv of recentPageviews) {\n pathCounts[pv.pathname] = (pathCounts[pv.pathname] || 0) + 1;\n }\n\n const topPages = Object.entries(pathCounts)\n .map(([path, count]) => ({ path, count }))\n .sort((a, b) => b.count - a.count)\n .slice(0, 10);\n\n return {\n activeUsers: activeSessions.size,\n pageviews: recentPageviews.length,\n topPages\n };\n }\n\n /**\n * Export data as JSON\n */\n exportData(): AnalyticsData {\n return { ...this.allData };\n }\n\n /**\n * Clear all analytics data\n */\n clearData(): void {\n this.buffer = { pageviews: [], vitals: [], events: [] };\n this.allData = { pageviews: [], vitals: [], events: [] };\n this.sessions.clear();\n }\n\n /**\n * Stop the analytics engine\n */\n stop(): void {\n if (this.flushTimer) {\n clearInterval(this.flushTimer);\n this.flushTimer = null;\n }\n this.flush();\n }\n}\n\n// ============================================================================\n// MIDDLEWARE\n// ============================================================================\n\nexport interface AnalyticsMiddlewareOptions {\n config?: Partial<AnalyticsConfig>;\n getCountry?: (req: Request) => string | undefined;\n}\n\nlet analyticsEngine: AnalyticsEngine | null = null;\n\n/**\n * Get or create analytics engine\n */\nexport function getAnalytics(config?: Partial<AnalyticsConfig>): AnalyticsEngine {\n if (!analyticsEngine) {\n analyticsEngine = new AnalyticsEngine(config);\n }\n return analyticsEngine;\n}\n\n/**\n * Configure analytics\n */\nexport function configureAnalytics(config: Partial<AnalyticsConfig>): AnalyticsEngine {\n analyticsEngine = new AnalyticsEngine(config);\n return analyticsEngine;\n}\n\n/**\n * Create analytics middleware\n */\nexport function createAnalyticsMiddleware(options: AnalyticsMiddlewareOptions = {}) {\n const engine = getAnalytics(options.config);\n\n return async (req: Request, next: () => Promise<Response>): Promise<Response> => {\n // Track pageview\n engine.trackPageview(req, {\n country: options.getCountry?.(req)\n });\n\n // Continue to next handler\n const response = await next();\n\n return response;\n };\n}\n\n/**\n * Create analytics API handler\n */\nexport function createAnalyticsHandler() {\n const engine = getAnalytics();\n\n return async (req: Request): Promise<Response> => {\n const url = new URL(req.url);\n const action = url.searchParams.get('action') || 'summary';\n\n switch (action) {\n case 'summary': {\n const startParam = url.searchParams.get('start');\n const endParam = url.searchParams.get('end');\n const summary = engine.getSummary(\n startParam ? new Date(startParam) : undefined,\n endParam ? new Date(endParam) : undefined\n );\n return new Response(JSON.stringify(summary), {\n headers: { 'Content-Type': 'application/json' }\n });\n }\n\n case 'realtime': {\n const realtime = engine.getRealtime();\n return new Response(JSON.stringify(realtime), {\n headers: { 'Content-Type': 'application/json' }\n });\n }\n\n case 'vitals': {\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 });\n }\n try {\n const body = await req.json() as { pathname: string; metrics: WebVitals['metrics'] };\n engine.trackVitals(body.pathname, body.metrics);\n return new Response(JSON.stringify({ success: true }), {\n headers: { 'Content-Type': 'application/json' }\n });\n } catch {\n return new Response('Invalid body', { status: 400 });\n }\n }\n\n case 'event': {\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 });\n }\n try {\n const body = await req.json() as { \n name: string; \n properties?: Record<string, string | number | boolean> \n };\n engine.trackEvent(body.name, body.properties, req);\n return new Response(JSON.stringify({ success: true }), {\n headers: { 'Content-Type': 'application/json' }\n });\n } catch {\n return new Response('Invalid body', { status: 400 });\n }\n }\n\n case 'export': {\n const data = engine.exportData();\n return new Response(JSON.stringify(data), {\n headers: { \n 'Content-Type': 'application/json',\n 'Content-Disposition': 'attachment; filename=\"analytics-export.json\"'\n }\n });\n }\n\n default:\n return new Response('Unknown action', { status: 400 });\n }\n };\n}\n\n// ============================================================================\n// CLIENT-SIDE SCRIPT (for Web Vitals)\n// ============================================================================\n\nexport const analyticsClientScript = `\n<script>\n(function() {\n // Simple Web Vitals reporter\n const endpoint = '/__float/analytics?action=vitals';\n \n function sendVitals(metrics) {\n const body = JSON.stringify({\n pathname: window.location.pathname,\n metrics: metrics\n });\n \n if (navigator.sendBeacon) {\n navigator.sendBeacon(endpoint, body);\n } else {\n fetch(endpoint, { method: 'POST', body, keepalive: true });\n }\n }\n\n // Observe LCP\n if ('PerformanceObserver' in window) {\n const vitals = {};\n \n // LCP\n new PerformanceObserver((list) => {\n const entries = list.getEntries();\n const lastEntry = entries[entries.length - 1];\n vitals.LCP = Math.round(lastEntry.startTime);\n }).observe({ type: 'largest-contentful-paint', buffered: true });\n \n // FCP\n new PerformanceObserver((list) => {\n const entries = list.getEntries();\n vitals.FCP = Math.round(entries[0].startTime);\n }).observe({ type: 'paint', buffered: true });\n \n // CLS\n let clsValue = 0;\n new PerformanceObserver((list) => {\n for (const entry of list.getEntries()) {\n if (!entry.hadRecentInput) {\n clsValue += entry.value;\n }\n }\n vitals.CLS = clsValue;\n }).observe({ type: 'layout-shift', buffered: true });\n \n // Send on page hide\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') {\n sendVitals(vitals);\n }\n });\n }\n})();\n</script>\n`;\n\n// ============================================================================\n// MAIN EXPORT\n// ============================================================================\n\nexport const analytics = {\n engine: getAnalytics,\n configure: configureAnalytics,\n createMiddleware: createAnalyticsMiddleware,\n createHandler: createAnalyticsHandler,\n clientScript: analyticsClientScript,\n track: {\n pageview: (req: Request, options?: { country?: string }) => \n getAnalytics().trackPageview(req, options),\n event: (name: string, properties?: Record<string, string | number | boolean>, req?: Request) => \n getAnalytics().trackEvent(name, properties, req),\n vitals: (pathname: string, metrics: WebVitals['metrics']) => \n getAnalytics().trackVitals(pathname, metrics)\n }\n};\n\nexport default analytics;\n"],"mappings":"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","names":[]}
|