Skip to content

Commit 5181852

Browse files
committed
dep better-auth
1 parent 64620b5 commit 5181852

File tree

14 files changed

+447
-106
lines changed

14 files changed

+447
-106
lines changed

apps/basket/src/lib/request-validation.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,21 @@ export function validateRequest(
4646
return { error: { status: "error", message: "Payload too large" } };
4747
}
4848

49-
const clientId = sanitizeString(
49+
let clientId = sanitizeString(
5050
query.client_id,
5151
VALIDATION_LIMITS.SHORT_STRING_MAX_LENGTH
5252
);
53+
54+
if (!clientId) {
55+
const headerClientId = request.headers.get("databuddy-client-id");
56+
if (headerClientId) {
57+
clientId = sanitizeString(
58+
headerClientId,
59+
VALIDATION_LIMITS.SHORT_STRING_MAX_LENGTH
60+
);
61+
}
62+
}
63+
5364
if (!clientId) {
5465
logBlockedTraffic(
5566
request,

apps/basket/src/routes/basket.ts

Lines changed: 135 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,11 @@ function processErrorEventData(
151151
ip: string
152152
): Promise<ErrorEvent> {
153153
return record("processErrorEventData", async () => {
154-
const payload = errorData.payload;
155-
const eventId = parseEventId(payload.eventId, randomUUID);
154+
// Support both envelope format (payload) and direct format
155+
const payload = errorData.payload || errorData;
156+
const eventId = parseEventId(payload.eventId || errorData.eventId, randomUUID);
156157
const now = Date.now();
157-
const timestamp = parseTimestamp(payload.timestamp);
158+
const timestamp = parseTimestamp(payload.timestamp || errorData.timestamp);
158159

159160
const [geoData, uaData] = await Promise.all([
160161
getGeo(ip),
@@ -170,25 +171,25 @@ function processErrorEventData(
170171
client_id: clientId,
171172
event_id: eventId,
172173
anonymous_id: sanitizeString(
173-
payload.anonymousId,
174+
payload.anonymousId || errorData.anonymousId,
174175
VALIDATION_LIMITS.SHORT_STRING_MAX_LENGTH
175176
),
176-
session_id: validateSessionId(payload.sessionId),
177+
session_id: validateSessionId(payload.sessionId || errorData.sessionId),
177178
timestamp,
178-
path: sanitizeString(payload.path, VALIDATION_LIMITS.STRING_MAX_LENGTH),
179+
path: sanitizeString(payload.path || errorData.path, VALIDATION_LIMITS.STRING_MAX_LENGTH),
179180
message: sanitizeString(
180-
payload.message,
181+
payload.message || errorData.message,
181182
VALIDATION_LIMITS.STRING_MAX_LENGTH
182183
),
183184
filename: sanitizeString(
184-
payload.filename,
185+
payload.filename || errorData.filename,
185186
VALIDATION_LIMITS.STRING_MAX_LENGTH
186187
),
187-
lineno: payload.lineno,
188-
colno: payload.colno,
189-
stack: sanitizeString(payload.stack, VALIDATION_LIMITS.STRING_MAX_LENGTH),
188+
lineno: payload.lineno || errorData.lineno,
189+
colno: payload.colno || errorData.colno,
190+
stack: sanitizeString(payload.stack || errorData.stack, VALIDATION_LIMITS.STRING_MAX_LENGTH),
190191
error_type: sanitizeString(
191-
payload.errorType,
192+
payload.errorType || errorData.errorType,
192193
VALIDATION_LIMITS.SHORT_STRING_MAX_LENGTH
193194
),
194195
ip: anonymizedIP || "",
@@ -212,10 +213,11 @@ function processWebVitalsEventData(
212213
ip: string
213214
): Promise<WebVitalsEvent> {
214215
return record("processWebVitalsEventData", async () => {
215-
const payload = vitalsData.payload;
216-
const eventId = parseEventId(payload.eventId, randomUUID);
216+
// Support both envelope format (payload) and direct format
217+
const payload = vitalsData.payload || vitalsData;
218+
const eventId = parseEventId(payload.eventId || vitalsData.eventId, randomUUID);
217219
const now = Date.now();
218-
const timestamp = parseTimestamp(payload.timestamp);
220+
const timestamp = parseTimestamp(payload.timestamp || vitalsData.timestamp);
219221

220222
const [geoData, uaData] = await Promise.all([
221223
getGeo(ip),
@@ -231,17 +233,17 @@ function processWebVitalsEventData(
231233
client_id: clientId,
232234
event_id: eventId,
233235
anonymous_id: sanitizeString(
234-
payload.anonymousId,
236+
payload.anonymousId || vitalsData.anonymousId,
235237
VALIDATION_LIMITS.SHORT_STRING_MAX_LENGTH
236238
),
237-
session_id: validateSessionId(payload.sessionId),
239+
session_id: validateSessionId(payload.sessionId || vitalsData.sessionId),
238240
timestamp,
239-
path: sanitizeString(payload.path, VALIDATION_LIMITS.STRING_MAX_LENGTH),
240-
fcp: validatePerformanceMetric(payload.fcp),
241-
lcp: validatePerformanceMetric(payload.lcp),
242-
cls: validatePerformanceMetric(payload.cls),
243-
fid: validatePerformanceMetric(payload.fid),
244-
inp: validatePerformanceMetric(payload.inp),
241+
path: sanitizeString(payload.path || vitalsData.path || vitalsData.url, VALIDATION_LIMITS.STRING_MAX_LENGTH), // Support both path and url
242+
fcp: validatePerformanceMetric(payload.fcp || vitalsData.fcp),
243+
lcp: validatePerformanceMetric(payload.lcp || vitalsData.lcp),
244+
cls: validatePerformanceMetric(payload.cls || vitalsData.cls),
245+
fid: validatePerformanceMetric(payload.fid || vitalsData.fid),
246+
inp: validatePerformanceMetric(payload.inp || vitalsData.inp),
245247
ip: "",
246248
user_agent: "",
247249
country: country || "",
@@ -299,6 +301,112 @@ function processOutgoingLinkData(
299301
}
300302

301303
const app = new Elysia()
304+
.post("/vitals", async (context) => {
305+
const { body, query, request } = context as {
306+
body: any;
307+
query: any;
308+
request: Request;
309+
};
310+
311+
try {
312+
const validation = await validateRequest(body, query, request);
313+
if ("error" in validation) {
314+
return validation.error;
315+
}
316+
317+
const { clientId, userAgent, ip } = validation;
318+
319+
const [botError, parseResult] = await Promise.all([
320+
checkForBot(request, body, query, clientId, userAgent),
321+
validateEventSchema(
322+
webVitalsEventSchema,
323+
body,
324+
request,
325+
query,
326+
clientId
327+
),
328+
]);
329+
330+
if (botError) {
331+
return botError.error;
332+
}
333+
334+
if (!parseResult.success) {
335+
return createSchemaErrorResponse(parseResult.error.issues);
336+
}
337+
338+
const vitalsEvent = await processWebVitalsEventData(
339+
body,
340+
clientId,
341+
userAgent,
342+
ip
343+
);
344+
345+
await insertWebVitals(vitalsEvent, clientId, userAgent, ip);
346+
return { status: "success", type: "web_vitals" };
347+
348+
} catch (error) {
349+
captureError(error, { message: "Error processing vitals" });
350+
return { status: "error", message: "Internal server error" };
351+
}
352+
})
353+
.post("/errors", async (context) => {
354+
const { body, query, request } = context as {
355+
body: any;
356+
query: any;
357+
request: Request;
358+
};
359+
360+
try {
361+
const validation = await validateRequest(body, query, request);
362+
if ("error" in validation) {
363+
return validation.error;
364+
}
365+
366+
const { clientId, userAgent, ip } = validation;
367+
368+
if (FILTERED_ERROR_MESSAGES.has(body.message)) {
369+
return {
370+
status: "ignored",
371+
type: "error",
372+
reason: "filtered_message",
373+
};
374+
}
375+
376+
const [botError, parseResult] = await Promise.all([
377+
checkForBot(request, body, query, clientId, userAgent),
378+
validateEventSchema(
379+
errorEventSchema,
380+
body,
381+
request,
382+
query,
383+
clientId
384+
),
385+
]);
386+
387+
if (botError) {
388+
return botError.error;
389+
}
390+
391+
if (!parseResult.success) {
392+
return createSchemaErrorResponse(parseResult.error.issues);
393+
}
394+
395+
const errorEvent = await processErrorEventData(
396+
body,
397+
clientId,
398+
userAgent,
399+
ip
400+
);
401+
402+
await insertError(errorEvent, clientId, userAgent, ip);
403+
return { status: "success", type: "error" };
404+
405+
} catch (error) {
406+
captureError(error, { message: "Error processing error" });
407+
return { status: "error", message: "Internal server error" };
408+
}
409+
})
302410
.post("/", async (context) => {
303411
const { body, query, request } = context as {
304412
body: any;
@@ -362,7 +470,8 @@ const app = new Elysia()
362470
return createSchemaErrorResponse(parseResult.error.issues);
363471
}
364472

365-
insertError(body, clientId, userAgent, ip);
473+
const errorEvent = await processErrorEventData(body, clientId, userAgent, ip);
474+
insertError(errorEvent, clientId, userAgent, ip);
366475
return { status: "success", type: "error" };
367476
}
368477

@@ -386,7 +495,8 @@ const app = new Elysia()
386495
return createSchemaErrorResponse(parseResult.error.issues);
387496
}
388497

389-
insertWebVitals(body, clientId, userAgent, ip);
498+
const vitalsEvent = await processWebVitalsEventData(body, clientId, userAgent, ip);
499+
insertWebVitals(vitalsEvent, clientId, userAgent, ip);
390500
return { status: "success", type: "web_vitals" };
391501
}
392502

apps/dashboard/app/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ export default function RootLayout({
126126
maskPatterns={["/websites/***"]}
127127
scriptUrl={
128128
isLocalhost
129-
? "http://localhost:3000/databuddy.js"
129+
? "https://databuddy.b-cdn.net/databuddy.js"
130130
: "https://cdn.databuddy.cc/databuddy.js"
131131
}
132132
skipPatterns={[]}

apps/dashboard/public/databuddy.js

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -948,29 +948,55 @@
948948
}
949949

950950
try {
951-
const metrics = { fcp: null, lcp: null, cls: 0, fid: null, inp: null };
952-
let reported = false;
953-
951+
const metrics = { fcp: undefined, lcp: undefined, cls: undefined, fid: undefined, inp: undefined, ttfb: undefined };
952+
954953
const clamp = (v) =>
955954
typeof v === 'number' ? Math.min(60_000, Math.max(0, v)) : v;
956955

957-
const report = () => {
958-
if (
959-
reported ||
960-
!Object.values(metrics).some((m) => m !== null && m !== 0)
961-
) {
956+
const sendVitals = () => {
957+
if (!Object.values(metrics).some((m) => m !== undefined)) {
962958
return;
963959
}
964-
reported = true;
965-
this.trackWebVitals({
960+
961+
const payload = {
962+
eventId: generateUUIDv4(),
963+
anonymousId: this.anonymousId,
964+
sessionId: this.sessionId,
966965
timestamp: Date.now(),
967966
fcp: clamp(metrics.fcp),
968967
lcp: clamp(metrics.lcp),
969-
cls: metrics.cls,
970-
fid: metrics.fid,
968+
cls: clamp(metrics.cls),
971969
inp: metrics.inp,
970+
ttfb: clamp(metrics.ttfb),
971+
url: window.location.href,
972+
};
973+
974+
this.sendBeacon({
975+
type: 'web_vitals',
976+
payload
972977
});
973-
this.cleanupWebVitals();
978+
};
979+
980+
// Send initial report after a delay to let FCP/LCP settle
981+
setTimeout(() => {
982+
sendVitals();
983+
}, 4000);
984+
985+
const report = () => {
986+
sendVitals();
987+
};
988+
989+
// Debounce reporting to avoid multiple triggers close together
990+
let reportTimeout;
991+
const debouncedReport = (immediate = false) => {
992+
if (reportTimeout) {
993+
window.clearTimeout(reportTimeout);
994+
}
995+
if (immediate) {
996+
report();
997+
} else {
998+
reportTimeout = window.setTimeout(report, 1000);
999+
}
9741000
};
9751001

9761002
const observe = (type, callback) => {
@@ -1005,7 +1031,7 @@
10051031
observe('layout-shift', (entries) => {
10061032
for (const entry of entries) {
10071033
if (!entry.hadRecentInput) {
1008-
metrics.cls += entry.value;
1034+
metrics.cls = (metrics.cls || 0) + entry.value;
10091035
}
10101036
}
10111037
});
@@ -1024,26 +1050,29 @@
10241050
}
10251051
}
10261052
});
1053+
1054+
// Navigation Timing API for TTFB
1055+
try {
1056+
const navEntry = performance.getEntriesByType('navigation')[0];
1057+
if (navEntry) {
1058+
metrics.ttfb = Math.round(navEntry.responseStart - navEntry.requestStart);
1059+
}
1060+
} catch (e) {}
10271061

10281062
this.webVitalsVisibilityChangeHandler = () => {
10291063
if (document.visibilityState === 'hidden') {
1030-
report();
1064+
debouncedReport(true);
10311065
}
10321066
};
1067+
10331068
document.addEventListener(
10341069
'visibilitychange',
1035-
this.webVitalsVisibilityChangeHandler,
1036-
{
1037-
once: true,
1038-
}
1070+
this.webVitalsVisibilityChangeHandler
10391071
);
10401072

1041-
this.webVitalsPageHideHandler = report;
1042-
window.addEventListener('pagehide', this.webVitalsPageHideHandler, {
1043-
once: true,
1044-
});
1073+
this.webVitalsPageHideHandler = () => debouncedReport(true);
1074+
window.addEventListener('pagehide', this.webVitalsPageHideHandler);
10451075

1046-
this.webVitalsReportTimeoutId = setTimeout(report, 10_000);
10471076
} catch (_e) {
10481077
//
10491078
}

apps/docs/app/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export default function Layout({ children }: { children: ReactNode }) {
9898
trackAttributes
9999
trackErrors
100100
trackOutgoingLinks
101-
trackWebVitals
101+
trackWebVitals
102102
/>
103103
<body>
104104
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>

0 commit comments

Comments
 (0)