Skip to content

Semver policy

This page is the developer-friendly summary of the in-tree docs/VERSIONING.md. That document is the authority — every commitment below is bound by what it says.

ArtefactVersioningSource of truth
SDK packages (@syntarie/tracking, @syntarie/tracking-node, @syntarie/shared)Strict semvernpm + the API contract §1
Collector + Query API HTTPURL-prefix versioning (/v1/…)The API contract §2 + §3
Postgres schemaAppend-only with respect to in-support SDKsThe migrations directory
Tracking planCustomer-ownedThe plan-diff tool

Required for any of:

  • A symbol listed in the API contract is removed or renamed.
  • A required parameter is added.
  • A required parameter changes type to a non-supertype.
  • A return type narrows in a way that breaks consumers.
  • A previously-narrow union widens ('cookied' | 'cookieless'string).
  • A default behaviour reverses (e.g. respectDnt flipping from true to false would be a privacy regression and is breaking).
  • A runtime / browser target is dropped (e.g. raising the Safari floor above 15, or the Node floor above 20).
  • A new export (function, type, interface, subpath).
  • A new optional parameter with a back-compatible default.
  • A new optional field on an interface.
  • A new tracking-plan event surfaced via @syntarie/tracking/generated/events.

A minor MAY introduce a deprecation notice on an existing export — but the export itself MUST keep working through the rest of the major. Removal is a major bump.

  • Bug fix that does not change documented behaviour.
  • Performance improvement with no observable surface change.
  • Internal refactor that keeps every documented contract stable.
  • Doc / typing tightening that narrows an unknown to its already-promised type.

Every subpath under @syntarie/tracking/<name> shares the version of the parent package. There is no per-subpath version — a breaking change in @syntarie/tracking/vitals is a major bump of @syntarie/tracking as a whole.

Bundle size is a CI gate, not a semver gate

Section titled “Bundle size is a CI gate, not a semver gate”

Bundle budgets (< 5 kB gzip core, < 1 kB / ≤ 1.5 kB per opt-in) are enforced in CI as deployment gates. A bug fix that bumps gzip size triggers a code review (the reviewer may require a refactor) — it does NOT trigger a major bump.

Collector + Query API — URL-prefix versioning

Section titled “Collector + Query API — URL-prefix versioning”

Every endpoint listed in the API contract is reachable at BOTH the existing unprefixed path AND the new /v1/... path. The unprefixed paths are aliases — adding the alias at v1.0 prevents breaking every customer who pinned the v0.9 paths.

Canonical (v1.0+)Alias (kept through v1.x)
POST /v1/eventsPOST /events
POST /v1/s2s/eventsPOST /s2s/events
GET /v1/healthzGET /healthz
GET /v1/metricsGET /metrics
GET /v1/workspaces, …GET /workspaces, …
GET /v1/sites/:siteId/events, …GET /sites/:siteId/events, …
POST /v1/webhooks/:adapter/:siteIdPOST /webhooks/:adapter/:siteId

New endpoints land only under /v1/. The unprefixed alias is a back-compat affordance for the v0.9-era paths; new surface area does not get a parallel alias.

  • Adding a new endpoint under /v1/.
  • Adding an optional query param, header, or request-body field.
  • Adding a new field to a response body (callers tolerate unknown JSON keys).
  • Adding a new accepted enum value if existing values keep their semantics.
  • Adding a new error code for a previously-unrepresented failure mode.

Requires a /v2/ namespace and a deprecation cycle on /v1/:

  • Removing a response field.
  • Renaming a response field.
  • Changing the type of a response field.
  • Changing a status code for an existing condition.
  • Changing an auth model.
  • Removing an endpoint OR removing an unprefixed alias.
  • Tightening request validation in a way that rejects a previously-accepted payload.

Per RFC 8594, every deprecated /v1/ endpoint emits:

Deprecation: <http-date>
Sunset: <http-date>
Link: <https://syntarie.dev/migrate/v1-v2>; rel="deprecation"

Deprecation period is at least 12 months between announcement and sunset. The sunset MUST land in a major release — never inside v1.x.

{ "error": { "code": "<stable_string>", "message": "", "details": [/* optional */] } }

code is part of the contract. message is human-readable and may evolve. details may gain new structured entries.

Append-only with respect to columns written by any SDK still in support. The platform supports the N-2 SDK majors at the storage layer — at v1.x that is @syntarie/tracking@0.x and @syntarie/tracking@1.x.

  • Add a column. New SDK populates; old leaves NULL.
  • Add an index. Operators create concurrently in production.
  • Add a new table.
  • Widen a column type so all existing values remain valid.
  • Add a constraint that current data already satisfies.
  • Rename a column an in-support SDK writes.
  • Drop a column an in-support SDK writes.
  • Tighten a constraint that current data violates (any NOT NULL on a previously-nullable column with extant NULLs is breaking).
  • Change semantic meaning of an existing column without a name change.

Every event lands in events.raw as the verbatim wire JSON. This is intentional: it lets columnar fields be backfilled without re-ingesting from the SDK. Adding a new top-level event field at the wire layer means the collector can persist the value into events.raw immediately, and a follow-up migration that promotes it into a typed column can backfill from raw without losing any historical events.

This safety net is a hard requirement — every collector release must continue to write the full payload to raw, even fields it doesn’t yet understand.

The tracking-plan YAML format and parser follow the same semver as the rest of @syntarie/shared. Customer plans evolve under their own breaking-change policy — the plan-diff tool enforces it in CI.

The platform itself does not constrain how customers evolve their plans. A customer adding purchase_v2 while keeping purchase is non-breaking from the platform’s perspective; whether it is breaking for their downstream analytics consumers is the customer’s decision, surfaced by the diff tool.

Every release ships a CHANGELOG.md entry following Keep a Changelog.

  • Breaking changes — only present in major releases.
  • Added — new public surface.
  • Changed — observable behaviour change that is not breaking.
  • Deprecated — newly-deprecated surface, with the planned removal version.
  • Removed — only present in major releases.
  • Fixed — bug fixes.
  • Security — security fixes, always backported to the most recent minor of every supported major.
  • Performance — bundle size deltas, p99 deltas. Reviewer-required when a budget moves.