Method: Simple Z-Score
For each (project, connector, metric, date), we compute:rolling_meanandrolling_stddevare computed over a trailing 30-day window (configurable:ANOMALY_BASELINE_WINDOW, default 29 days preceding + current row)- Source:
fact_daily_kpisingle-canonical-dimension totals (anti-double-count guard)
dbt/models/marts/metric_baselines.sql and dbt/models/marts/anomalies_daily.sql.
Anti-Double-Count Guard
fact_daily_kpi contains parallel breakdown_dimension series (e.g. device_category AND country) that EACH independently total the day. Summing across ALL breakdown rows would produce values 2-5x too large. metric_baselines.sql uses the canonical_dim pattern (same as cross_source_conversions.sql) to pick ONE breakdown_dimension per (project, connector, metric) deterministically (MIN(breakdown_dimension)), then aggregates only over that dimension’s rows. This is an architectural requirement (AD-13, review-1-6 lesson).
Threshold
An anomaly is flagged when|zscore| >= ANOMALY_Z_THRESHOLD (default 3.0).
|z|in[3.0, 5.0)→ severity:warning|z| >= 5.0→ severity:error
ANOMALY_Z_THRESHOLD is configurable via environment variable (see .env.example).
Why Z-Score?
- Transparent: no model parameters to tune, no black box. The exact formula is visible in the dbt model SQL (AD-9 requirement).
- Robust for marketing KPIs at day grain.
- Testable: the seed generator (
generate_seed.py) injects a 3x sessions spike at day 75 (AI-09), producing a z-score well above 3.0 for deterministic test validation.
Limitations
- Requires at least 2 days of history (sample stddev is undefined with 1 row). Rows with
rolling_stddev IS NULL OR = 0are excluded — no division by zero, no false alerts during the first day of data. - Sensitive to sudden baseline shifts (e.g. new campaign launch). Use
context_eventsto annotate known shifts. - Does not detect gradual drift (use declared thresholds for that: Story 5.3).
Context Event Citations (AD-9)
Anomalies never assert a cause. When context events exist for the anomaly date, they are listed as candidate context only. When absent, “missing context” is stated. This is an AD-9 architectural requirement. Prohibited language in any generated message:- “caused by”, “due to”, “because of”, “as a result of”
test_causal_language_absent in server/tests/core/test_anomaly_alerts.py) enforces this constraint.
Pull ID Provenance for Anomaly Firings
Anomaly firings havepull_ids = [] (empty array). Unlike business threshold firings which trace to specific raw data pull batches, anomalies are derived signals computed by the anomalies_daily dbt model from historical data. Provenance is the dbt model itself (documented method above), not a raw data event.

