Documentation menu

Events

The observation layer: one RebilderEventV0 per handled request, a frozen v0 schema, and sinks that can never break your serving path.

One event per handled request

Every request the gateway handles — including pass-throughs — emits exactly one RebilderEventV0 when onEvent is configured: requester from detection, request (url, accept, referrer), and response.path with measured render_ms. Emission is fire-and-forget: never awaited, sync throws and async rejections are swallowed; a broken or slow event sink can neither block nor break a response.

The schema is frozen at v0 and additive-only: new fields extend a version or start a new one, and a v0 consumer must accept events produced by a later additive revision (unknown extra keys are allowed at every level). This feed is what the Console renders — agent visits by platform, what they saw, how fast.

RebilderEventV0 field reference

FieldTypeNotes
event_idstringGlobally unique event id — the idempotency key through the queue and ingest.
tsstringEvent timestamp, ISO 8601 UTC.
store_idstringThe storeId from your gateway config. On the hosted ingest API, the store resolved from your API key always overrides this.
requesterobjectWho made the request (fields below).
requester.kind'agent' | 'human' | 'protocol' | 'crawler'Crawlers ride the human/HTML serving path but are recorded first-class, so they stay distinguishable from humans in the warehouse.
requester.platformstring (optional)Present when identifiable: 'chatgpt', 'gemini', 'claude', 'perplexity' are known literals; the set is open — any newly observed platform string is valid (additive schema change).
requester.verifiedbooleanTrue only for cryptographically verified agents (Web Bot Auth — the gateway’s verification key registry). The shipped registry is empty by design, so this stays false until the operator populates keys — see Protocols.
requestobjectWhat was asked for (fields below).
request.urlstringThe canonical request URL (all adapters normalize to canonical storefront URLs).
request.intent_signalsRecord<string, unknown>Intent signals extracted from query/referrer/agent payload. Shape intentionally open in v0.
request.acceptstring (optional)Raw Accept header, when present (e.g. 'text/markdown').
request.referrerstring (optional)Referrer, when present.
responseobjectWhat was served (fields below).
response.path'markdown' | 'html-variant' | 'protocol'Which serving path answered. Pass-throughs are recorded as 'html-variant'.
response.variant_idstring (optional)Set when path is 'html-variant' and a composed variant was served — Phase 4; absent today.
response.render_msnumberServer-side render/serve time in milliseconds, measured with performance.now() (edge budget: p95 < 50ms).
outcomeobject (optional)Outcome facts joined asynchronously in the warehouse — never present at edge emission time. The order join is live: report orders via the outcomes API or the Shopify webhook — see Outcomes.
outcome.citedboolean (optional)The product was cited in an agent response.
outcome.referredboolean (optional)A visit was referred from an agent surface.
outcome.add_to_cartboolean (optional)Attributed add-to-cart.
outcome.purchaseboolean (optional)Attributed purchase.
outcome.order_valuenumber (optional)Order value in the store’s currency minor units, or as reported by the merchant platform.

Events record visits; orders arrive separately and are joined at read time with labeled evidence — the contract for reporting them (the POST /v1/outcomes API and the Shopify orders webhook) lives on the Outcomes page.

The wire protocol

Any conforming ingest implementation must accept exactly this:

POST /v1/events
POST {url}/v1/events
Authorization: Bearer <apiKey>
Content-Type: application/json

{ "events": RebilderEventV0[] }
  • Success: 202 Accepted with body { "accepted": <n> } — the client treats any 2xx as accepted and does not parse the body.
  • event_id is the idempotency key through the queue: the client retries a batch at most once, so the ingest side deduplicates on event_id.
  • 5xx / network error: the client retries the identical batch once after 500ms, then drops it.
  • 4xx: the client drops the batch immediately without retrying.

The hosted ingest API (https://api.rebilder.com) additionally responds 401 for an unknown, revoked, or missing API key; 400 for a malformed body (not an array, more than 500 events per request, or structural failure); and 503 when events ingest is not configured on that deployment. Keys are store-scoped: the resolved store_id always overrides any store_id in the payload, so a key can never write another store’s events.

createHttpEventSink

sink.ts
import { createHttpEventSink } from '@rebilder/events'

const sink = createHttpEventSink({
  url: 'https://api.rebilder.com', // ingest base — the sink POSTs to `${url}/v1/events`
  apiKey: process.env.REBILDER_API_KEY!, // sent as `Authorization: Bearer <apiKey>`
  // maxBatch: 20,          — flush as soon as the queue reaches this many events
  // flushIntervalMs: 2000, — flush timer while the queue is non-empty
  // fetchImpl: fetch,      — injectable for tests; defaults to globalThis.fetch
  // onError: console.warn, — called for every dropped event/batch; never rethrown
})

sink.emit(event) // sync enqueue; invalid events dropped via onError, never thrown
await sink.flush() // send the queue now; resolves even on failure
await sink.close() // flush remaining events + stop the timer; later emits are dropped
OptionTypeDefaultNotes
urlstringIngest base URL, e.g. https://api.rebilder.com — the sink POSTs to ${url}/v1/events.
apiKeystringSent as Authorization: Bearer <apiKey>.
maxBatchnumber20Flush as soon as the queue reaches this many events.
flushIntervalMsnumber2000Flush timer interval while the queue is non-empty.
fetchImpltypeof fetchglobalThis.fetchInjectable for tests.
onError(err: unknown) => voidconsole.warnCalled for every dropped event/batch (invalid event, transport failure after retry, 4xx rejection, queue overflow, emit after close). Errors it throws are swallowed — never rethrown into the caller.

Delivery semantics — observation must never break serving. emit() never throws, flush()/close() never reject; failures are reported through onError and the batch is dropped. The queue is capped at 1000 events — beyond that the oldest event is dropped via onError. emit() after close() drops the event.

Console sink

console sink
import { createConsoleEventSink } from '@rebilder/events'

const sink = createConsoleEventSink()
sink.emit(event) // console.info('[rebilder-event] {"event_id":...}')

One structured [rebilder-event] {json} line per event via console.info — useful before you have an API key (a log drain captures them). Upgrade path: swap createConsoleEventSink() for createHttpEventSink({...}) once you have ingest credentials; the event shape is already the v0 contract, so nothing else changes.

Wiring the sink into the gateway

lib/gateway-config.ts
import type { GatewayConfig } from '@rebilder/gateway'
import { createHttpEventSink } from '@rebilder/events'

const sink = createHttpEventSink({ url: 'https://api.rebilder.com', apiKey: '...' })

export const gatewayConfig: GatewayConfig = {
  storeId: 'my-store',
  sources: {
    /* ... */
  },
  onEvent: (event) => sink.emit(event),
}

Getting an API key

API keys are store-scoped and issued in the Console: create an account, add your store, and the onboarding flow issues a key (shown once — store it in your secret manager). Rotate or revoke any time in Console → Settings; a rotated key’s predecessor starts returning 401 immediately. The Help Center has a step-by-step walkthrough with screenshots.