BIO.RE
Analytics

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.

PATCH /api/v1/analytics/pageview/:id/leave — 🌐 Public · Rate limit: 10 req / minute

Finalizes the duration and scroll depth of a single pageview. Earlier pageviews in the same session get their duration back-filled automatically by the next POST /pageview, so this endpoint really only matters for the last pageview of a session — the one followed by a tab close, navigation away, or full app exit.

Use navigator.sendBeacon from pagehide / beforeunload. A regular fetch from those handlers usually gets cancelled by the browser. sendBeacon is queued by the browser and delivered after the page unloads. The body is JSON; the server will accept it.

duration is capped at 3600s server-side. Math.min(duration, 3600) — anything you pass over an hour gets clamped. scrollDepth is clamped to [0, 100] before write — pass percentages, not pixel values.

Silent failures. update.catch(() => {}) — if the pageview id is wrong or the row's been pruned, you still get 200 { ok: true } and no error.

Request

Path parameters

ParamTypeRequiredNotes
idstring (UUID)The pageview row id from POST /analytics/pageview. Not validated server-side — bad ids no-op silently.

Body — PageLeaveDto

FieldTypeRequiredValidationNotes
durationnumber (seconds)IsNumber Min(0) Max(3600)Whole or fractional seconds the page was visible. DTO validates [0, 3600]; service additionally Math.min(duration, 3600) as belt-and-braces.
scrollDepthnumber (percent)optionalIsNumber Min(0) Max(100)Furthest scroll position as a 0-100 percentage. DTO validates the range; service additionally Math.min(Math.max(value, 0), 100).

Headers

HeaderRequiredNotes
Content-Type: application/jsonsendBeacon will send application/x-www-form-urlencoded or Blob mime-type by default — wrap your JSON in a typed Blob to keep it application/json. See the sample.

No auth.

Response

200 OKApiResponseOf<PageLeaveResultDto>

{ "success": true, "data": { "ok": true } }

Same caveats as heartbeat — ok: true is "request reached service," not "row was updated."

Errors

HTTPReason
400DTO validation (duration outside [0, 3600], scrollDepth outside [0, 100], missing duration).
429Over 10 req/min from this IP.

Side effects

  1. ThrottleGuard (10 req / 60s).
  2. analyticsDb.analyticsPageView.update({ where: { id }, data: { duration: Math.min(data.duration, 3600), scrollDepth: clamped(0..100) ?? null } }).catch(() => {}). Single SQL UPDATE; mis-id silently swallowed.
  3. Return { ok: true }.

Code samples

curl -X PATCH https://api.bio.re/api/v1/analytics/pageview/pv-uuid/leave \
  -H 'Content-Type: application/json' \
  -d '{ "duration": 47, "scrollDepth": 65 }'
async function pageLeave(
  pageViewId: string,
  data: { duration: number; scrollDepth?: number },
): Promise<void> {
  await fetch(`https://api.bio.re/api/v1/analytics/pageview/${pageViewId}/leave`, {
    method: 'PATCH',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  }).catch(() => {});
}
// Fires reliably during page unload — fetch usually gets cancelled in unload handlers.
function leaveBeacon(
  pageViewId: string,
  data: { duration: number; scrollDepth?: number },
): boolean {
  const url = `/api/v1/analytics/pageview/${pageViewId}/leave`;
  const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
  return navigator.sendBeacon(url, blob);
}

// Wire it up — pagehide is more reliable than beforeunload on mobile Safari.
window.addEventListener('pagehide', () => {
  if (!lastPageViewId) return;
  const duration = Math.floor((Date.now() - mountedAt) / 1000);
  const scrollDepth = Math.round(
    (window.scrollY + window.innerHeight) / document.documentElement.scrollHeight * 100,
  );
  leaveBeacon(lastPageViewId, { duration, scrollDepth });
});

Try it

PATCH
/api/v1/analytics/pageview/{id}/leave

Path Parameters

id*string

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

application/json

curl -X PATCH "https://loading/api/v1/analytics/pageview/string/leave" \  -H "Content-Type: application/json" \  -d '{    "duration": 0  }'
{
  "success": true,
  "data": {
    "ok": true
  }
}

Source

SourcePathLines
Controllerapps/api-core/src/modules/analytics/analytics.controller.ts168–177 (pageLeave)
DTO (request)apps/api-core/src/modules/analytics/dto/tracking.dto.ts38–41 (PageLeaveDto)
DTO (response)apps/api-core/src/modules/analytics/dto/analytics-client-response.dto.ts43–46 (PageLeaveResultDto)
Serviceapps/api-core/src/modules/analytics/traffic-tracking.service.ts205–213 (pageLeave — duration cap + scrollDepth clamp + silent fail)
Prisma modelpackages/prisma-analytics/prisma/schema.prismaAnalyticsPageView.duration / scrollDepth lines 112–113

On this page