Skip to content

Validation

When SCHEMA_BUNDLE_PATH is set, the collector validates every incoming event against the per-event JSON Schema from your tracking plan. Bad events take one of two paths depending on the configured mode.

ModeBad-event responseStorageUse when
Reject400 schema_validation_failed with structured detailsNot persistedYou want producers to fix bad events before they ship.
Quarantine202 AcceptedPersisted to the dead-letter queue with the validation errorYou want lossless ingest with a manual review path.

Reject is correct for most product teams: the SDK retry treats 400 as permanent (no infinite retry loop), the structured details surface in the SDK’s console.warn, and the bad event never pollutes your analytics. Quarantine is correct when ingest is from third parties you cannot quickly fix.

In Reject mode the response body is:

{
"error": {
"code": "schema_validation_failed",
"message": "event failed schema validation",
"details": [
{ "path": "/total_cents", "message": "must be >= 0" },
{ "path": "/currency", "message": "must match pattern \"^[A-Z]{3}$\"" }
]
}
}

Paths are JSON Pointers into the event payload.

In Quarantine mode bad events land in the dead-letter queue table with their original wire JSON and the structured validation error. Operators can:

  • List entries: GET /sites/:siteId/dlq?limit=&cursor= (admin or admin-scoped api_key).
  • Re-POST a single entry to /events: POST /sites/:siteId/dlq/:eventId/replay. On a 202 the DLQ row is deleted; on any other collector status the row stays in place.

The replay path re-enters the validator, the consent gate, and the rate limiter — operators cannot bypass the wire contract by replaying.

See Dead-letter queue in the API reference for response shapes.

An event whose type is not in the plan is treated as an unknown event. Behaviour depends on mode:

  • Reject returns 400 schema_validation_failed with details: [{ path: "/type", message: "unknown event" }].
  • Quarantine persists the event to the DLQ with the same error.

If you want truly free-form custom events, declare a wildcard event in the plan (type: object, additionalProperties: true, required: []) — but do this knowingly, because it suppresses the schema discipline that motivates having a plan in the first place.

  • The collector does not validate envelope fields (id, site_id, anon_id, ts, url) against the plan. Those have their own canonical validation in @syntarie/shared/events.schema.json and are enforced at the wire boundary.
  • Customer plan changes are NOT enforced for backwards compatibility — the platform cannot decide what is breaking for your downstream consumers. That is the plan-diff tool’s job.

Validation runs in the hot path. The platform guarantees the collector p99 ingest latency stays under 5 ms (ex-network) with validation enabled. Schema bundles are precompiled at startup.