Session Heartbeat
Public keep-alive ping. Updates lastActivityAt and increments duration by a fixed 30 seconds. Designed to fire every 30s while the page is in the foreground.
PATCH /api/v1/analytics/session/:id/heartbeat โ ๐ Public ยท Rate limit: 5 req / 30 s
Marks the session as still active and adds 30 seconds to its rolling duration counter. Call once every ~30 seconds while the user has the page in the foreground; stop when they background the tab or close the page. The endpoint hard-codes a +30 increment regardless of how long it's actually been since the last beat, so don't call it more often than once per 30s โ you'll over-count time on session.
duration is incremented by a hard-coded 30s per call. The server does NOT measure the elapsed wall-clock time between heartbeats. Two beats in 1 second still add 60s to duration. The throttle (@Throttle(5, 30) โ 5 req per 30s) is what protects against accidental spam, but you should still pace your client to one beat per ~30s.
Failures are silent. Service does update.catch(() => {}) โ if the session id doesn't exist or DB is unhappy, you still get 200 { ok: true }. No way for the client to detect that the session is gone via this endpoint; only the session-create response's null id signals real rejection.
Pair with the Page Visibility API. The right cadence is "every 30 seconds while document.visibilityState === 'visible'." Sending heartbeats from a backgrounded tab inflates active-time metrics. See the code sample.
Request
Path parameters
| Param | Type | Required | Notes |
|---|---|---|---|
id | string | โ | Session id from POST /analytics/session. Not validated by ParseUUIDPipe โ any string is accepted; mismatches no-op. |
Headers
| Header | Required | Notes |
|---|---|---|
Content-Type: application/json | optional | Body is empty. |
No body, no auth.
Response
200 OK โ ApiResponseOf<HeartbeatResultDto>
{ "success": true, "data": { "ok": true } }ok: true means "request reached the service" โ not "DB write succeeded" (see warn callout).
Errors
| HTTP | Reason |
|---|---|
429 | Over 5 req / 30s from this IP. (Throttle is per-IP, not per-session โ multiple tabs share the budget.) |
Side effects
ThrottleGuard(5 req / 30s).analyticsDb.analyticsSession.update({ where: { id }, data: { lastActivityAt: new Date(), duration: { increment: 30 } } }).catch(() => {}). Single SQL UPDATE, no read-then-write.- Return
{ ok: true }regardless of whether any row was matched.
Code samples
curl -X PATCH https://api.bio.re/api/v1/analytics/session/ses-uuid/heartbeatasync function sessionHeartbeat(sessionId: string): Promise<void> {
await fetch(`https://api.bio.re/api/v1/analytics/session/${sessionId}/heartbeat`, {
method: 'PATCH',
}).catch(() => {}); // never throw from instrumentation
}// Fire a heartbeat every 30s, but only while the tab is visible.
// Pauses on visibilitychange = 'hidden', resumes on 'visible'.
function startHeartbeat(sessionId: string): () => void {
let timer: number | null = null;
const start = () => {
if (timer != null) return;
timer = window.setInterval(() => sessionHeartbeat(sessionId), 30_000);
};
const stop = () => {
if (timer != null) { clearInterval(timer); timer = null; }
};
const onVisibility = () => {
if (document.visibilityState === 'visible') start(); else stop();
};
document.addEventListener('visibilitychange', onVisibility);
if (document.visibilityState === 'visible') start();
// Return cleanup
return () => {
document.removeEventListener('visibilitychange', onVisibility);
stop();
};
}Try it
curl -X PATCH "https://loading/api/v1/analytics/session/string/heartbeat"{
"success": true,
"data": {
"ok": true
}
}Source
| Source | Path | Lines |
|---|---|---|
| Controller | apps/api-core/src/modules/analytics/analytics.controller.ts | 157โ166 (sessionHeartbeat) |
| DTO (response) | apps/api-core/src/modules/analytics/dto/analytics-client-response.dto.ts | 36โ39 (HeartbeatResultDto) |
| Service | apps/api-core/src/modules/analytics/traffic-tracking.service.ts | 195โ203 (heartbeat โ fixed +30 increment, silent failure) |
| Prisma model | packages/prisma-analytics/prisma/schema.prisma | AnalyticsSession.duration / lastActivityAt lines 53โ54 |
Record Page View
Public pageview endpoint. Atomic session-counter update, bounce-flag flip on the second view, automatic duration backfill on the previous view. Silently swallows errors.
Record Page Leave
Public exit-time ping for the LAST pageview of a session โ the one that won't get its duration back-filled by a subsequent pageview. Captures duration (capped 1h) and scroll depth (clamped 0-100). Use sendBeacon.