ScaleBun
Skip to article

SSR & edge runtimes

webDeveloper

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.

Updated Reviewed

init() behaves differently in three runtimes, and it detects which one it is in. You do not configure this.

RuntimeWhat happens
BrowserFull 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 / workerA 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.

TypeScript
// 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.jsexport { 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:

TypeScript
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#

SSR & edge runtimes · Integrations · Web SDK · ScaleBun