SSR & edge runtimes
What the SDK does during server rendering, how the edge and worker lite lane captures analytics without a DOM, and how to report server-side errors from Nuxt, SvelteKit or Angular Universal.
init() behaves differently in three runtimes, and it detects which one it is
in. You do not configure this.
| Runtime | What happens |
|---|---|
| Browser | Full capture. |
| SSR render pass (Node) | Deterministic no-op — no network, no observers, no storage. Capture starts on the client after hydration, where your client entry calls init() again. |
| Edge / worker | A lite lane: analytics and config send over fetch with an ephemeral identity. No DOM, so no session, replay, or persistence. |
Every framework adapter inherits the same guard, so calling init() from a
server component or a root layout that also renders on the server is safe.
Why edge is different from SSR#
An SSR render pass is followed by hydration — the same page continues in a browser, so deferring capture loses nothing. An edge function or worker handles a request and never hydrates. If capture were deferred there it would never happen at all, so the lite lane is the only capture those runtimes get.
// A worker or edge function — analytics and config work, nothing DOM-shaped does.await ScaleBun.init({ clientKey, apiBaseUrl });ScaleBun.track('api_request_served', { route: '/checkout' });Identity is ephemeral because there is no storage to persist a device id in, so these events are not stitched to a browser session.
Server-side errors#
The browser SDK covers the browser. An error thrown while rendering on the
server, in a route handler, or in an action needs its own path — the facade is
inert outside the browser, so captureError() there does nothing.
Next.js has this built in. See
Next.js — export { register, onRequestError }
from '@scalebun/web-next/server' and you are done.
For Nuxt, SvelteKit, Angular Universal or a bare Node server, POST the error
yourself. The endpoint takes a small JSON body and needs nothing but fetch:
async function reportServerError(error: unknown, route?: string): Promise<void> { const err = error as { message?: string; stack?: string } | undefined; try { await fetch(`${process.env.SCALEBUN_API_BASE_URL}/ingestion/server-errors`, { method: 'POST', headers: { 'content-type': 'application/json', 'x-scalebun-client-key': process.env.SCALEBUN_CLIENT_KEY!, }, body: JSON.stringify({ errors: [ { type: 'fatal', // fatal | handled | unhandledrejection message: err?.message ?? String(error), stack: err?.stack, route, clientId: crypto.randomUUID(), // idempotency for double-sends timestamp: Date.now(), }, ], runtime: 'nodejs', // nodejs | edge release: process.env.SCALEBUN_RELEASE, environment: process.env.SCALEBUN_ENV, }), signal: AbortSignal.timeout(2000), }); } catch { // Never let reporting break the response. }}Only errors[].message is required. The array is capped at 50 errors per
request, message at 8 KB and stack at 32 KB. clientId is what lets the
backend deduplicate an accidental double-send without merging two genuinely
distinct occurrences — send a fresh one per error, not per retry.
Wire it into your framework's error hook — SvelteKit's handleError in
hooks.server.ts, Nuxt's nitroApp.hooks.hook('error', …), or Angular
Universal's server error handler.
Three things worth copying from the Next reporter:
Use server-only environment variables. A
PUBLIC_-prefixed key is shipped to the browser; the server key should not be.Scrub before sending. Emails and secret-like tokens turn up in messages and stack frames more often than you would expect.
Never throw, and always time out. A reporting failure must not become a failed response.
Next#
Next.js — the built-in server integration.
Install & initialize — where
init()belongs.