Everything an SDK reports — an error, a log line, a span, a page view — reaches Buglapse as an event: one JSON object with a type and a type-specific payload. There are five types, each with its own page in this section:
| Type | Sent by | Stored as |
|---|---|---|
exception | captureException(), error integrations | an exception, grouped into an issue |
log | log.info() / log.warn() / … , ConsoleIntegration | a log row |
span | span() / asyncSpan() / startInactiveSpan() | a span, plus its metrics |
hit | HitIntegration, hit(name) (Browser) | a page view or custom hit |
replay | ReplayIntegration (Browser) | one segment of a session replay |
There's no event for a trace: a trace is just the traceId its spans share, and Buglapse creates it from the first span that arrives with a new one (see Span → Traces).
You never build these objects by hand — the SDK does — but knowing their shape helps when writing a preprocessor, a custom transport, or reading what the makeConsoleTransport prints.
The envelope
Every event, whatever its type, has the same five top-level fields:
json
{
"type": "log",
"timestamp": "2026-09-24T10:15:02.481Z",
"tags": { "checkout.step": "payment" },
"contexts": {
"environment": "production",
"release": "web@1.4.2",
"user.id": "42",
"trace.id": "5b7c0e1a-…"
},
"payload": { "level": "info", "message": "Order placed" }
}type— one ofexception,log,span,hit,replay. Anything else is rejected as invalid. (trace, sent by SDKs before 0.1.0, is accepted and silently ignored.)timestamp— ISO 8601 time the event happened on the client. Required; an unparseable value makes the event invalid. A value more than a minute ahead of the server's clock (a device whose clock runs fast) is replaced with the time the event arrived.tags— flatstring → stringmap fromsetTag()/scope.setTag(). Short, indexed values you filter by.contexts— flat map with dot-notation keys (user.email,order.total) fromsetContext(),setUser(),environment/releaseininit(), and integrations.nullvalues are dropped.payload— the type-specific part, described on each type's page.
Contexts the backend reads
Most contexts are stored as-is and shown on the event, but a few keys drive behavior:
environment,release— copied into their own columns; used by filters, the environment switcher, and release tracking.user.*(user.id,user.email,user.username,user.name) — identify the end user; events are linked to a project user. The backend always addsuser.ipanduser.agentfrom the request itself.device.userAgent— parsed on ingestion intobrowser.*,os.*anddevice.*contexts. Every other inbounddevice.*key is discarded.trace.id,span.id— stamped by the SDK on any event captured while a trace/span is active, so the dashboard can link a log or exception back to its trace.replay.id— set by ReplayIntegration, links an exception to the session replay it happened in.
Sending a batch
The SDK batches events client-side and posts them together (see Transports):
http
POST https://app.buglapse.com/api/envelope
X-Buglapse-Auth: Buglapse buglapse_key={key}
Content-Type: application/json
{ "events": [ { "type": "hit", … }, { "type": "log", … } ] }{key} is the user part of the DSN (https://{key}@{host}) — it alone identifies the project. A batch can mix types freely. navigator.sendBeacon() can't set headers, so the SDK passes the key as a ?buglapse_key={key} query parameter there instead. A missing or unknown key gets 401 ("Invalid DSN: unknown project or key."). The request isn't authenticated beyond that key — instead, its Origin/Referer must match the project's allowed domains (Settings → Security), or the whole request is rejected with 403.
A retry of a batch that already got through (the transport resends the identical body after a timeout) is recognized and answered with 202 and { "success": true, "duplicate": true } — nothing is stored or charged twice.
What happens to each event
Every event in the batch goes through the same steps, in order, and ends up in exactly one of four outcomes:
- Validation — a missing/unknown
typeor badtimestamp→ invalid. - Filters — ingestion turned off, a filtered IP/environment/release, a known crawler, or a type-specific rule (see
exception) → filtered. - Sampling — the project's server-side sample rate for that type (Settings → Filters) → filtered.
- Rate limits — the project's hourly caps (Settings → Filters → Rate limits) → rate limited. Rate-limited events don't spend the quota.
- Quota — the organization's monthly plan limit for that type (errors, spans, logs, metrics, replays — each its own pool, shared by every project) → quota exceeded.
- Otherwise the event is stored → processed.
Filtered, invalid, rate-limited, and over-quota events are not stored, but they are counted on the Stats & Usage page (rate-limited and over-quota together, as Rate limited). The response reports the tally for the batch:
json
{ "success": true, "processed": 18, "filtered": 1, "quota_exceeded": 0, "rate_limited": 0, "invalid": 1 }The status is 200, or 202 when the server queues accepted events for writing instead of storing them during the request — the tally is the same either way, but a 202's processed events can take a moment to show up. Treat both as success.
If nothing in the batch got through and at least one event hit a rate limit or quota, the response is 429 instead, with a Retry-After header. While the server's ingestion queue is backlogged, it also turns away span and replay events this way (before they cost quota), so errors and logs keep flowing.
Rate limits
Two hourly caps protect the quota from a sudden flood — say, every page view throwing the same error while an API is down:
- Spike protection (on by default) caps each of
exception,log,hit,replayandspanseparately at twice its average hourly volume over the previous two hours, and never below 1000 per hour. The average only counts accepted events, so a spike can't raise its own cap: the cap grows by roughly half each hour, leaving hours to notice and fix the cause. It doesn't apply until the project has events older than two hours, so a new install isn't capped. - Events per hour (off by default) is a fixed cap on the project's events of all types together, spans excluded.
Both reset at the top of every hour (UTC). Whenever a type hits a rate limit or a quota, the response — even a 200/202 — carries which types to hold off on and for how many seconds:
X-Buglapse-Rate-Limits: 1740:exception;logThe SDK's transport drops those types client-side until then instead of sending batches that would only be rejected. A custom client should do the same.
Quotas at a glance
Every stored event counts toward the monthly limit of its own type — each a separate pool shared by all of the organization's projects, so a flood of one type never blocks another:
| Type | Counts toward |
|---|---|
exception | monthly errors limit |
log | monthly logs limit |
hit | monthly metrics limit |
span | monthly spans limit — metrics attached to the span cost nothing extra |
replay | monthly replay limit — one per segment |