Skip to content

Commit 4889d2c

Browse files
📝 Add docstrings to codex/implement-web-vitals-tracking-and-api-jt9zi9
Docstrings generation was requested by @shayancoin. * #125 (comment) The following files were modified: * `frontend/src/app/api/otel/webvital/route.ts` * `frontend/src/app/providers.tsx` * `frontend/src/app/web-vitals.ts`
1 parent c08538a commit 4889d2c

File tree

3 files changed

+95
-3
lines changed

3 files changed

+95
-3
lines changed

frontend/src/app/api/otel/webvital/route.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,34 @@ const DEFAULT_SCOPE = {
2727
version: '1.0.0',
2828
};
2929

30+
/**
31+
* Converts a millisecond timestamp to a string representing nanoseconds since the Unix epoch.
32+
*
33+
* @param ms - Milliseconds since Unix epoch; negative values are treated as zero and fractional milliseconds are floored.
34+
* @returns The timestamp in nanoseconds as a decimal string.
35+
*/
3036
function toUnixNano(ms: number) {
3137
const time = BigInt(Math.max(0, Math.floor(ms))) * NANOSECONDS_IN_MILLISECOND;
3238
return time.toString();
3339
}
3440

41+
/**
42+
* Generate a base64-encoded cryptographic random identifier.
43+
*
44+
* @param bytes - Number of random bytes to generate
45+
* @returns A base64-encoded string containing `bytes` cryptographically secure random bytes
46+
*/
3547
function randomId(bytes: number) {
3648
return randomBytes(bytes).toString('base64');
3749
}
3850

51+
/**
52+
* Determines the OTLP collector endpoint URL from environment variables.
53+
*
54+
* Prefers `OTEL_EXPORTER_OTLP_ENDPOINT` or `OTEL_COLLECTOR_URL` when present. If those are not set, builds a URL from `OTEL_COLLECTOR_HOST` with optional `OTEL_COLLECTOR_PROTOCOL` (default `http`), `OTEL_COLLECTOR_PORT`, and `OTEL_COLLECTOR_PATH` (default `/v1/traces`).
55+
*
56+
* @returns The collector endpoint URL as a string, or `undefined` if no configuration is available.
57+
*/
3958
function resolveCollectorEndpoint() {
4059
const explicit =
4160
process.env.OTEL_EXPORTER_OTLP_ENDPOINT || process.env.OTEL_COLLECTOR_URL;
@@ -56,6 +75,13 @@ function resolveCollectorEndpoint() {
5675
return `${protocol}://${host}${portSegment}${path}`;
5776
}
5877

78+
/**
79+
* Create an OTLP span object representing a single web-vital event.
80+
*
81+
* @param event - Web-vital event data whose fields are mapped to span attributes (e.g., `webvital.value`, `webvital.delta`, `webvital.rating`, `webvital.id`, `webvital.type`, `webvital.navigation_type`, `navigation.*`, and truncated `webvital.attribution`)
82+
* @param fallbackTime - Fallback timestamp in milliseconds since epoch used to compute the span's start and end times when precise timing is not provided by the event
83+
* @returns An OTLP-compatible span object containing `traceId`, `spanId`, `name`, `kind`, `startTimeUnixNano`, `endTimeUnixNano`, `traceState`, `flags`, and an `attributes` array describing the web-vital and navigation details
84+
*/
5985
function createSpan(event: IncomingEvent, fallbackTime: number) {
6086
const duration =
6187
typeof event.detail?.duration === 'number' && event.detail.duration > 0
@@ -150,6 +176,17 @@ function createSpan(event: IncomingEvent, fallbackTime: number) {
150176
};
151177
}
152178

179+
/**
180+
* HTTP POST handler that ingests a JSON payload of web-vital events and exports them as OTLP spans to a configured collector.
181+
*
182+
* Processes an incoming payload of events, transforms each event into an OTLP span, sends the batch to the resolved collector endpoint, and returns a JSON response describing the result.
183+
*
184+
* @returns A JSON response:
185+
* - `{ accepted: number }` when spans were successfully exported (number is the count of accepted spans).
186+
* - `{ error: 'Invalid JSON payload', details: string }` with HTTP status 400 if the request body is invalid JSON.
187+
* - `{ error: 'Collector endpoint is not configured' }` with HTTP status 500 if no collector endpoint is available.
188+
* - `{ error: 'Failed to export web vitals', status: number, body: string }` with HTTP status 502 if the collector responds with a non-OK status.
189+
*/
153190
export async function POST(request: Request) {
154191
let payload: IncomingPayload;
155192
try {
@@ -227,4 +264,3 @@ export async function POST(request: Request) {
227264

228265
return NextResponse.json({ accepted: spans.length });
229266
}
230-

frontend/src/app/providers.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,18 @@ const theme = extendTheme({
2121
},
2222
});
2323

24+
/**
25+
* Wraps the application UI with a ChakraProvider using the project theme and initializes web vitals on mount.
26+
*
27+
* Calls `initWebVitals` once when mounted to start web vitals collection.
28+
*
29+
* @param children - The React node(s) to render within the themed provider
30+
* @returns The React element tree where `children` are wrapped by a `ChakraProvider` configured with the module theme
31+
*/
2432
export function Providers({ children }: { children: React.ReactNode }) {
2533
useEffect(() => {
2634
initWebVitals();
2735
}, []);
2836

2937
return <ChakraProvider theme={theme}>{children}</ChakraProvider>;
30-
}
38+
}

frontend/src/app/web-vitals.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ const queue: WebVitalPayload[] = [];
3737
let flushTimeout: number | undefined;
3838
let initialized = false;
3939

40+
/**
41+
* Schedules a batch flush of queued web-vital events to occur after the configured interval if none is already scheduled.
42+
*
43+
* When the timer fires, the queued events are flushed and the scheduled handle is cleared.
44+
*/
4045
function scheduleFlush() {
4146
if (flushTimeout) {
4247
return;
@@ -48,6 +53,13 @@ function scheduleFlush() {
4853
}, BATCH_INTERVAL_MS);
4954
}
5055

56+
/**
57+
* Adds a web vital or navigation timing payload to the in-memory queue and triggers batching behavior.
58+
*
59+
* If the queue reaches the configured maximum batch size, a flush is initiated immediately; otherwise a flush is scheduled.
60+
*
61+
* @param payload - The WebVitalEvent or NavigationTimingEvent to enqueue for later delivery
62+
*/
5163
function enqueue(payload: WebVitalPayload) {
5264
queue.push(payload);
5365

@@ -59,6 +71,13 @@ function enqueue(payload: WebVitalPayload) {
5971
scheduleFlush();
6072
}
6173

74+
/**
75+
* Sends all queued web-vital and navigation-timing events to the server and clears the in-memory queue.
76+
*
77+
* If the queue is empty this is a no-op. Attempts to deliver using `navigator.sendBeacon` when available;
78+
* otherwise falls back to a POST request with `keepalive`. On network failure, logs a warning in non-production
79+
* environments and restores the events to the front of the queue so they are not lost.
80+
*/
6281
async function flushQueue() {
6382
if (!queue.length) {
6483
return;
@@ -98,6 +117,14 @@ async function flushQueue() {
98117
}
99118
}
100119

120+
/**
121+
* Collects navigation timing metrics from the Performance API and enqueues a navigation-timing payload.
122+
*
123+
* If the Performance API is not available or no navigation entry exists, this function is a no-op.
124+
*
125+
* The enqueued payload contains the navigation `duration`, `navigationType`, and a `detail` object with
126+
* `domContentLoaded`, `firstByte`, `responseTime`, `loadEvent`, and `duration` deltas.
127+
*/
101128
function collectNavigationTimings() {
102129
if (typeof performance === 'undefined' || !performance.getEntriesByType) {
103130
return;
@@ -129,6 +156,12 @@ function collectNavigationTimings() {
129156
});
130157
}
131158

159+
/**
160+
* Convert a web-vitals Metric into a WebVitalEvent payload suitable for batching and transmission.
161+
*
162+
* @param metric - Metric object from the web-vitals library to convert
163+
* @returns A WebVitalEvent containing the metric's name, value, identifiers, rating, and optional `navigationType` and `attribution` when present
164+
*/
132165
function metricToPayload(metric: Metric): WebVitalEvent {
133166
const payload: WebVitalEvent = {
134167
type: 'web-vital',
@@ -152,6 +185,11 @@ function metricToPayload(metric: Metric): WebVitalEvent {
152185
return payload;
153186
}
154187

188+
/**
189+
* Starts observing core web-vital metrics and enqueues each metric as a telemetry payload.
190+
*
191+
* Registers observers for CLS, LCP, INP, FCP, and TTFB; each observed metric is converted to a payload and added to the batching queue for delivery.
192+
*/
155193
function registerWebVitalObservers() {
156194
const handleMetric = (metric: Metric) => {
157195
enqueue(metricToPayload(metric));
@@ -164,6 +202,11 @@ function registerWebVitalObservers() {
164202
onTTFB(handleMetric);
165203
}
166204

205+
/**
206+
* Register page lifecycle listeners that trigger an immediate flush of the web-vitals queue.
207+
*
208+
* Adds handlers for `beforeunload` and `pagehide`, and for `visibilitychange` when the document becomes `hidden`, causing the queued events to be sent before the page is unloaded or hidden.
209+
*/
167210
function setupLifecycleFlushes() {
168211
const immediateFlush = () => {
169212
void flushQueue();
@@ -178,6 +221,12 @@ function setupLifecycleFlushes() {
178221
});
179222
}
180223

224+
/**
225+
* Initializes collection and reporting of web-vitals and navigation timing data.
226+
*
227+
* This function is idempotent: calling it multiple times has no additional effect.
228+
* It is a no-op outside of a browser environment.
229+
*/
181230
export function initWebVitals() {
182231
if (initialized || typeof window === 'undefined') {
183232
return;
@@ -189,4 +238,3 @@ export function initWebVitals() {
189238
registerWebVitalObservers();
190239
setupLifecycleFlushes();
191240
}
192-

0 commit comments

Comments
 (0)