Skip to content

API Contracts

This page describes the HTTP and WebSocket contracts that vite-plugin-monitor's client uses to communicate with the backend. Implement these contracts when using mode: 'http' or mode: 'websocket'.

Ingest Endpoint (HTTP)

Used when storage.mode = 'http'. The browser POSTs batched events to this endpoint.

Request

POST <storage.writeEndpoint>
Content-Type: application/json
X-Tracker-Key: <storage.apiKey>    (only when apiKey is configured)

{
  "type": "ingest",
  "events": TrackerEvent[]
}

Headers:

HeaderValueWhen
Content-Typeapplication/jsonAlways
X-Tracker-KeyThe configured apiKeyOnly when storage.apiKey is set
Content-EncodinggzipWhen the browser supports the CompressionStream API

Request body compression

When the browser supports the CompressionStream API (Chrome 80+, Firefox 113+, Safari 16.4+), the client gzip-compresses the request body before sending it and adds Content-Encoding: gzip to the request headers.

If the API is not available, the body is sent as plain JSON with no Content-Encoding header. Your ingest handler must support both cases.

Compression is skipped in the following situations, regardless of browser support:

  • When keepalive: true is used (flush triggered by visibilitychange or beforeunload)
  • When navigator.sendBeacon is used (page unload fallback)

If you implement a custom backend for mode: 'http', decompress the body when Content-Encoding: gzip is present:

typescript
// Node.js / TypeScript
import { gunzip } from 'node:zlib'
import { promisify } from 'node:util'

const gunzipAsync = promisify(gunzip)

async function parseBody(req: IncomingMessage): Promise<string> {
  const chunks: Buffer[] = []
  for await (const chunk of req) {
    chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
  }
  const raw = Buffer.concat(chunks)
  if (req.headers['content-encoding'] === 'gzip') {
    const decompressed = await gunzipAsync(raw)
    return decompressed.toString('utf8')
  }
  return raw.toString('utf8')
}

Body:

The events array contains one or more TrackerEvent objects. A single flush can contain up to storage.batchSize events (default: 25).

Assign id on every ingested event

The browser client always sends events with id: "" (an empty string). Your ingest handler must assign a unique, non-empty id to every event before persisting it — for example:

typescript
// Node.js / TypeScript
import { randomUUID } from 'node:crypto'

for (const event of body.events) {
  event.id = randomUUID()
  await db.collection('events').insertOne(event)
}

Any unique string format is valid: UUID v4, MongoDB ObjectId, ULID, etc. The dashboard requires a non-empty id on every event to identify table rows without serializing the full payload. The built-in middleware mode handles this automatically.

Response

StatusBehavior
2xx (any)Success — batch is acknowledged and removed from the client queue
Non-2xxFailure — batch is re-queued and retried on the next flush interval

Minimal success response:

HTTP/1.1 200 OK

No response body is required. The client ignores the response body for the ingest endpoint.

Page Unload

On page unload (beforeunload), the client sends remaining events via navigator.sendBeacon using a Blob with Content-Type: application/json:

POST <writeEndpoint>
Content-Type: application/json

{ "type": "ingest", "events": TrackerEvent[] }

Content-Type on Beacon requests

The plugin wraps the payload in a Blob({ type: 'application/json' }) before passing it to navigator.sendBeacon. This causes the browser to send the request with Content-Type: application/json. The body is never compressed for beacon requests — sendBeacon does not support custom headers, so Content-Encoding cannot be set. Your backend does not need special handling for beacon requests beyond standard JSON parsing.


Read Endpoint (HTTP)

Used by the dashboard to query events. Required if you want the dashboard to display events in mode: 'http'.

Request

GET <storage.readEndpoint>?since=<ISO8601>&until=<ISO8601>
Accept: application/json
X-Tracker-Key: <storage.apiKey>    (only when apiKey is configured)

Query Parameters:

ParameterFormatDescription
sinceISO 8601 UTCStart of time range (inclusive)
untilISO 8601 UTCEnd of time range (inclusive)

The dashboard always sends both since and until. Your server must filter events to the [since, until] window and return them newest first (descending by timestamp).

Example request:

GET /tracker/events?since=2024-03-15T00:00:00.000Z&until=2024-03-15T23:59:59.999Z
Accept: application/json
X-Tracker-Key: tk_prod_xxxx

Response

HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: *

{
  "events": TrackerEvent[],
  "total":  123,
  "page": 1,
  "limit": 5
}
FieldTypeDescription
eventsTrackerEvent[]Events in the time range, newest first
totalnumberTotal count of events in the time range

Gzip compression in middleware mode

The built-in middleware (mode: 'middleware') compresses the read endpoint response with gzip (level 1) and adds Content-Encoding: gzip to the response headers. The dashboard handles this transparently via the browser's native fetch decompression.

If you implement a custom backend (mode: 'http' or mode: 'websocket'), you may return an uncompressed response — gzip is optional. The dashboard's fetch call sets no Accept-Encoding header explicitly and relies on the browser's default negotiation.

Pagination

The dashboard currently does not use server-side pagination — it loads all events for the selected time range in a single request. For large time ranges (e.g. 30d with millions of events), you may want to implement server-side aggregation or return a limited sample.

The total field should reflect the full count even if you return a subset of events.

Client-side filtering

Your server only needs to implement time-range filtering (since/until). All other filtering (by type, level, userId, route, full-text search) and all aggregations (KPI cards, charts, top lists) are performed client-side in the browser dashboard. Do not implement these on the server.


Ping Endpoint (HTTP)

Used by the dashboard's health check indicator. Any URL that returns 2xx is sufficient.

GET <storage.pingEndpoint>

→ 200 OK (or any 2xx)

The dashboard polls this endpoint to determine whether the backend is reachable and shows a coloured status dot (🟢 online / 🔴 offline).

If pingEndpoint is not configured, no request is made and the backend is assumed to be online — the status dot always shows 🟢 green.


WebSocket Protocol

Used when storage.mode = 'websocket'. All messages are JSON strings sent over a single persistent WebSocket connection.

The client connects to storage.wsEndpoint (wss://...) and uses the same connection for both event ingest and dashboard queries.

Connection

When apiKey is configured, immediately after the connection is established, the client sends: { "type": "auth", "key": "<storage.apiKey>" }

The server must respond with:

Until authentication succeeds, the server must reject all other messages and may close the connection with code 1008.

If no apiKey is configured, no auth message is sent and the connection is immediately ready for ingest and query messages. The client reconnects automatically with a fixed 3-second delay on disconnect.


Ingest — Browser → Server

The browser sends batched events:

json
{
  "type": "ingest",
  "events": TrackerEvent[]
}

Assign id on every ingested event

Events arrive with id: "". Your server must assign a unique id to each event before persisting it (e.g. randomUUID()). See the Ingest Endpoint warning for a complete example.


Ingest ACK — Server → Browser

The server acknowledges receipt:

json
{
  "type":  "ack",
  "saved": 42
}
FieldTypeDescription
type"ack"Message discriminant
savednumberNumber of events successfully persisted

If no ACK is received within the flush timeout, the batch is re-queued.


Dashboard Query — Dashboard → Server

The dashboard requests events for a time range:

json
{
  "type":  "events:query",
  "reqId": "550e8400-e29b-41d4-a716-446655440000",
  "query": {
    "since": "2024-03-15T00:00:00.000Z",
    "until": "2024-03-15T23:59:59.999Z"
  }
}
FieldTypeDescription
type"events:query"Message discriminant
reqIdstringUUID — correlates the response to this request
query.sinceISO 8601Start of time range
query.untilISO 8601End of time range

Dashboard Response — Server → Dashboard

The server responds to a query:

json
{
  "type":  "events:response",
  "reqId": "550e8400-e29b-41d4-a716-446655440000",
  "response": {
    "events": TrackerEvent[],
    "total":  123
  }
}
FieldTypeDescription
type"events:response"Message discriminant
reqIdstringMust match the reqId from the query
response.eventsTrackerEvent[]Events in range, newest first
response.totalnumberTotal count in range

Real-Time Push — Server → Browser (Optional)

The server can push new events to all connected clients in real time:

json
{
  "type":   "push",
  "events": TrackerEvent[]
}

When the dashboard receives a push message while in Live mode, it merges the new events into the current dataset without a full re-query. This enables true real-time updates without polling.

This message is optional — the dashboard works correctly without it (it falls back to polling).


Middleware Mode Endpoints

In middleware mode, the plugin implements all endpoints internally on the Vite dev server. These are the auto-configured values:

EndpointValue
Ingest (POST)/_tracker/events
Read (GET)/_tracker
Ping (GET)/_tracker/ping

Read endpoint response compression

GET /_tracker returns a gzip-compressed JSON response (Content-Encoding: gzip, level 1). Browsers decompress it transparently. This is handled automatically by the built-in dashboard — no configuration required.

The ping endpoint in middleware mode returns:

json
{
  "ok":    true,
  "appId": "my-app",
  "mode":  "middleware",
  "version": "0.1.0"
}

Released under the MIT License.