Analytics schemas usually drift silently: an event is renamed in one client, a
required property disappears, or a campaign field changes type before anyone
notices the broken dashboard. event-contracts gives growth engineers one
versioned tracking plan they can enforce locally or in CI.
It validates local JSON only. Runtime code uses the Python standard library and has no registry, network call, telemetry, event storage, or analytics property-value echoing in reports.
- Strict
lower_snake_caseevent names that exist in the plan - Required and optional
string,number,boolean,array, andobjectproperties - Scalar enums that match the declared type
- Explicit allow/reject policy for additional event properties
- Unknown or misspelled tracking-plan keys
- Direct-identifier classification, including identifiers nested in objects and arrays
- Exact event envelopes containing only
eventandproperties - Stable JSON paths and deterministic issue ordering
Python 3.12 or newer is required.
This project is not published to PyPI. Install the v0.1.0 wheel from its
GitHub release:
python -m pip install \
https://github.com/iamroylim/event-contracts/releases/download/v0.1.0/event_contracts-0.1.0-py3-none-any.whl
event-contracts validate \
--plan tracking-plan.json \
--events events.jsonl \
--format jsonl \
--prettyFor development from a source checkout:
python -m pip install -e ".[dev]"
event-contracts validate \
--plan examples/tracking-plan.json \
--events examples/events.jsonl \
--format jsonlOr install the tagged source directly:
python -m pip install \
"event-contracts @ git+https://github.com/iamroylim/[email protected]"The input format is explicit rather than inferred from a filename:
jsonrequires one JSON array of event objects.jsonlrequires one JSON event object per non-empty line.- Every event has exactly
{"event":"event_name","properties":{...}}.
The CLI prints one JSON report to stdout:
| Exit | Meaning |
|---|---|
0 |
All events satisfy the tracking plan |
1 |
Inputs are readable, but contract issues were found |
2 |
Usage, file, encoding, or JSON parsing failed |
Parse errors report only a filename and, for JSONL, a line number. Validation issues include schema names and paths but never analytics property values. The CLI caps JSON nesting at 512 levels so behavior remains predictable across supported Python versions; the public API's iterative privacy scan remains safe for deeper in-memory values.
examples/tracking-plan.json is complete:
{
"version": "1.0.0",
"events": {
"signup_completed": {
"additionalProperties": false,
"properties": {
"plan": {
"type": "string",
"required": true,
"enum": ["free", "pro"],
"privacy": "non_personal"
}
}
}
}
}Versions use MAJOR.MINOR.PATCH. Each event must declare
additionalProperties; omitted required defaults to optional. Enums support
string, number, and boolean.
Privacy classifications are non_personal, pseudonymous, sensitive, and
direct_identifier. Names that normalize exactly to email, emailAddress,
phone, phoneNumber, ip, ipAddress, or fullName must use
direct_identifier. Normalization ignores case, underscores, hyphens, and
spaces.
The privacy scan follows nested objects and arrays with an iterative,
cycle-safe traversal. A nested identifier is allowed only when its top-level
contracted property is classified direct_identifier. The rule deliberately
does not inspect scalar contents, so classify a scalar array as
direct_identifier when it intentionally carries direct identifiers.
from event_contracts import ValidationReport, validate_events
plan = {
"version": "1.0.0",
"events": {
"checkout_completed": {
"additionalProperties": False,
"properties": {
"order_id": {
"type": "string",
"required": True,
"privacy": "pseudonymous",
}
},
}
},
}
report: ValidationReport = validate_events(
plan,
[
{
"event": "checkout_completed",
"properties": {"order_id": "ord_123"},
}
],
)validate_tracking_plan(plan) accepts an untrusted parsed object and returns a
tuple of typed ValidationIssue values. validate_events(plan, events) returns
an immutable typed ValidationReport; report.to_dict() produces the CLI
shape.
python -m pytest
ruff check .
ruff format --check .
mypy src
python -m build
pip-audit --localMIT