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, and 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, so any newly observed platform string is valid (additive schema change).
requester.verifiedbooleanTrue only for cryptographically verified agents (Web Bot Auth, via 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). The query string is filtered at emission: only page, variant, sku, lang, locale and currency survive (lower-cased and sorted); the fragment and any userinfo are dropped. Tracking params, reset-password and magic-link tokens never leave your server, and free-text search never rides the URL. It is recorded only in intent_signals.query, after the fail-closed screen described in Agent intent.
request.intent_signalsRecord<string, unknown>Intent signals extracted from the request (events v0.4): the well-known keys are query (search text, recorded only after a fail-closed PII screen), query_param, referrer_platform, utm_source, utm_medium, tool, and result_count. Shape stays open; see Agent intent.
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. No emitter sets it today, so this is absent today.
response.render_msnumberServer-side render/serve time in milliseconds, measured with performance.now() (edge budget: p95 < 50ms).
response.sourcestring (optional)Which configured source answered: 'product', 'policies', 'catalog', 'document', 'collection', or 'none' when the markdown path ran and nothing matched. Absent when the markdown path was never attempted. Open set, like requester.platform: a future source kind is an additive change.
response.coverage'sourced' | 'unsourced' | 'not-applicable' (optional)Did a configured source resolve for this URL? 'unsourced' means an agent asked for markdown and every configured resolver returned null; that single bit is the Agent Miss Report. 'not-applicable' covers humans, crawlers, and protocol routes, where no source was consulted. Read this, not response.path: a miss is recorded with path: 'html-variant' because the gateway passed through to your HTML.
outcomeobject (optional)Outcome facts joined asynchronously in the warehouse (citation checks, referral attribution, order webhooks), so it is 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.

Coverage: what an agent asked for and did not get

Every event carries response.coverage. sourced means a configured source answered in markdown. unsourced means an agent asked for markdown and every configured resolver returned null, so the gateway passed through to your HTML and the agent got a page it has to guess at. not-applicable means no source was consulted at all: humans, search crawlers, and protocol routes.

Read coverage, not path. A miss is emitted with response.path: "html-variant", because passing through to your canonical HTML is exactly what the gateway did, so path alone cannot tell a human page view apart from an unanswered agent request. That is the whole reason coverage is a separate field, and it is the entire input to the Agent Miss Report: no sampling, no model, no second request, and it works at N = 1 on your own traffic.

When coverage is sourced, response.source names which resolver answered (product, policies, catalog, document, collection), which is useful for spotting a URL served by a lower-precedence source than you intended. When it is unsourced, source is "none".

Both fields are additive and optional, added in @rebilder/events 0.2.0. A gateway on an earlier version emits events without them and they stay valid; nothing about the v0 contract was removed, narrowed, or made required.

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
urlstring(required)Ingest base URL, e.g. https://api.rebilder.com; the sink POSTs to ${url}/v1/events.
apiKeystring(required)Sent 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, which is 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, so 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.