Spoar

Server

Track from route handlers and server actions with the secret key.

import { createServerAnalytics } from "@spoar/sdk/server";

export const serverAnalytics = createServerAnalytics<Events>({
  secret: process.env.RA_SECRET,
  endpoint: "https://api.analytics.remcostoeten.nl",
});

export const POST = serverAnalytics.withErrors(async (request) => {
  const result = await serverAnalytics.track("signup", { plan: "pro" }, { request });
  if (!result.ok) console.warn(result.error.code);
  return Response.json({ ok: true });
});
MethodDoes
track(name, props?, options?)Sends a custom event
identify(userId, traits?, options?)Links a user to the visitor in options
group(type, id, traits?, options?)Sends a group event in that group; createServerAnalytics<Events, Groups> types type and traits
captureError(error, context?)Records a server error; context takes the error context and the request context (request or headers) in one object
withErrors(handler)Wraps a route handler and records anything it throws, then rethrows
OptionWhereDoes
secret, endpointClientThe secret key (sk_...) and the API's base URL
releaseClientAttached to every event
originClient or callThe site's origin, such as https://example.com, sent as Origin when there is no request or headers to take it from. The API reads the host and the localhost and preview flags from it
fetch, waitUntilClientA custom fetch, and a function that keeps the send alive after the response
request or headersCallThe incoming request, for the visitor's IP and user agent, the site's origin and the admin session cookie
visitor, sessionCallThe browser's ids, to join the event to its visit
path, groups, waitUntilCallThe page path (default: the request's path, or /), groups, and a per-call waitUntil

Passing request or headers forwards the visitor's IP and user agent, so the API can hash and enrich them. It also sends the site's origin as Origin, from x-forwarded-host and x-forwarded-proto, then host, then the request URL, and the admin session cookie ra.session_token (or __Secure-ra.session_token) and no other cookie, so a signed-in admin's events are internal. An origin on the call wins over the derived one, which wins over the client's origin. Without request or headers the event has no IP and no user agent, and the API does not use the calling server's own: such an event gets no IP hash, no location and no bot weight for them.

Events from one tick go out together, in one request per origin and session cookie, and waitUntil from the options, the call or Vercel's runtime keeps the send alive after the response. Every method resolves to { ok, error, accepted, duplicates, failed } and never throws. Server events use server as visitor and session unless the call passes visitor and session; the entry exports that id as serverVisitor. Reads count those events in pageviews, events and breakdowns, but never as a visitor or a session. Any call can pass groups: { company: "acme" } in its options to put that event in groups. Options left out are read from the JSON in RA_CONFIG.

Alert webhooks

A webhook alert target posts a signed WebhookBody to your app. alertRoute turns it into a route handler with one typed function per event; in Next.js it is the whole route.ts:

import { alertRoute } from "@spoar/sdk/server";

export const POST = alertRoute({
  secret: process.env.RA_WEBHOOK_SECRET,
  on: {
    "issue.new": async (event) => notifyTeam(event.issue.title),
    "issue.regression": async (event) => openTicket(event.issue),
    "speed.drop": async (event) => notifyTeam(`Speed ${event.speed.previous} to ${event.speed.score}`),
  },
});
  • It checks x-analytics-signature, the HMAC-SHA256 of <x-analytics-timestamp>.<body> with the target's secret, and rejects a timestamp more than 5 minutes off, so a captured request cannot be replayed. A request that fails gets 401 and no handler runs; an empty secret answers 500.
  • on is typed per event, so event.issue and event.speed autocomplete, and a handler for an unknown event is a type error. An event without a handler is acknowledged and ignored.
  • verifyAlert(request, secret) is the same check without the routing, for other frameworks. It answers the parsed body or an error with the code NO_SECRET, BAD_SIGNATURE, STALE_TIMESTAMP or BAD_BODY.

The body is { v: 1, sentAt, events }, where each event is { name, project, issue: { id, title, culprit, level, count, firstSeen, lastSeen, lastRelease, url } }.

On this page