An anomaly is a value that differs materially from an explicit expectation. It is not simply a point that looks surprising on a chart.
GraphJSON can query historical time series, visualize comparisons, and alert on supported collection visualizations. It does not currently train or evaluate a built-in statistical anomaly model over arbitrary saved SQL.
Use one of two patterns:
GraphJSON SQL → investigate and compare with an explainable baseline
external evaluator → emit anomaly measurement
→ GraphJSON dashboard and supported threshold alert
Keep urgent paging, acknowledgment, and escalation in the monitoring system that owns the response.
Define the signal before the model#
Record:
| Field | Example |
|---|---|
| Metric | Completed report generations |
| Grain | 15-minute UTC bucket |
| Population | Production, non-test accounts |
| Unit | Successful workflows |
| Expected seasonality | Time of day and day of week |
| Settling delay | 10 minutes |
| Minimum denominator | 100 attempts |
| Owner | Reporting platform |
| Response | Check ingestion, deployment, queue, and provider health |
If no decision or investigation follows an anomaly, do not create an alert.
Distinguish anomaly types#
| Type | Example |
|---|---|
| Level shift | Conversion falls from 32% to 24% after a release |
| Spike | Webhook failures rise for one 15-minute interval |
| Trend change | Queue latency grows over several days |
| Seasonal deviation | Monday signups are low relative to prior Mondays |
| Missing signal | Evaluator or producer stopped sending |
| Composition shift | Total is stable but one plan or region changes |
| Data-quality anomaly | Event volume rises because of duplicate delivery |
The response differs. A missing signal should not be treated as a healthy zero, and a duplicate spike should not trigger a product-growth celebration.
Start with direct thresholds#
Use a fixed threshold when the acceptable boundary is known:
payment failure rate > 2%
oldest queue age > 15 minutes
data delivery lag > 10 minutes
checkout completion rate < 20%
A business or reliability boundary is easier to explain and test than a statistical score.
Use GraphJSON’s native alert surface for supported Single Line collection visualizations. Review the evaluation and notification limitations in Create and operate alerts.
Compare with the previous period#
For a first exploration, compare equivalent buckets:
WITH daily AS (
SELECT
toDate(toDateTime(timestamp)) AS day,
count() AS value
FROM product_events
WHERE
JSONExtractString(json, 'event') = 'report_published'
AND timestamp >= toUnixTimestamp(now() - INTERVAL 60 DAY)
GROUP BY day
)
SELECT
current.day,
current.value,
previous.value AS value_7_days_ago,
current.value - previous.value AS absolute_change,
if(
previous.value = 0,
NULL,
(current.value - previous.value) / previous.value
) AS relative_change
FROM daily AS current
LEFT JOIN daily AS previous
ON previous.day = current.day - INTERVAL 7 DAY
ORDER BY current.day
Same-weekday comparison accounts for weekly seasonality better than comparing Monday with Sunday. It still fails around holidays, launches, outages, and rapid growth.
Never divide by zero and label the result an infinite anomaly. Return a
documented no_baseline state.
Use several comparable periods#
A more stable weekly baseline can use the same weekday from several prior weeks:
expected = median(value 7, 14, 21, and 28 days earlier)
Median resists one abnormal historical period better than a mean. Also retain:
- number of baseline observations
- minimum and maximum
- median absolute deviation or another dispersion measure
- which comparison dates were used
Do not calculate a baseline from periods that include the value being tested. That reduces the apparent deviation.
Add a robust deviation score#
For a baseline median m and median absolute deviation MAD:
robust_score = 0.6745 × (observed - m) / MAD
This is a screening score, not a probability. Handle:
MAD = 0 and observed = median → score 0
MAD = 0 and observed differs → explicit zero-variance deviation
too few baseline periods → insufficient_baseline
Do not claim “99% confidence” from a score without a statistical model and validated assumptions.
For ratios, retain the numerator and denominator. A conversion-rate swing from one conversion out of two visitors is not comparable to a swing over 100,000 visitors.
Treat missing and zero separately#
Possible bucket states:
observed_zero
no_eligible_events
source_incomplete
evaluator_missing
not_applicable
Generate a complete time spine when you need to reveal missing buckets, then
left join the observed values. Do not use coalesce(value, 0) until the metric
contract says an absent row means a real zero.
A stopped heartbeat, rollup job, or ingestion pipeline is often more urgent than an unusual value. Monitor freshness independently:
{
"event": "metric_evaluation_completed",
"metric": "checkout_completion_rate",
"window_end": 1785081600,
"computed_at": 1785081900,
"data_complete_through": 1785081600
}
Account for seasonality and change#
Potential baseline dimensions:
- hour of day
- day of week
- business day vs weekend
- holiday or planned closure
- plan, region, platform, or release
- product maturity and sustained growth
Do not create a separate model for every high-cardinality segment. Require enough observations and a clear response owner.
When the product grows rapidly, a four-week fixed baseline can lag the current level. Compare rates, use shorter recent periods, or model a trend explicitly. Document when the baseline was reset.
Add annotations to the investigation#
Correlate anomalies with:
- releases and feature rollouts
- provider incidents
- schema and instrumentation changes
- campaigns
- holidays
- billing or pricing changes
- backfills and replays
Send bounded change events such as:
{
"event": "analytics_annotation",
"annotation_type": "release",
"release": "2026.07.26.3",
"service": "checkout",
"change_id": "deploy_91"
}
Do not use free-form annotation text as a chart split. Keep details in the deployment or change-management system.
Investigate in a fixed order#
When a signal is unusual:
- verify freshness and completeness
- check duplicates, schema changes, and collection routing
- inspect numerator and denominator
- compare the same segment and time definition
- check release and provider annotations
- drill down by a few approved dimensions
- sample stable event IDs
- compare with an independent source
- decide whether the product, pipeline, or expectation changed
Do not tune the threshold before explaining the event. Repeated unexplained alerts are evidence that the signal, baseline, or response is poorly designed.
Emit evaluated anomalies#
When a scheduled evaluator owns the baseline:
{
"event": "metric_anomaly_evaluated",
"measurement_id": "checkout_rate:15m:1785081600:v3",
"metric": "checkout_completion_rate",
"window_start": 1785080700,
"window_end": 1785081600,
"observed": 0.182,
"expected": 0.311,
"lower_bound": 0.264,
"upper_bound": 0.356,
"deviation_score": -4.2,
"numerator": 182,
"denominator": 1001,
"baseline_version": 3,
"baseline_periods": 8,
"status": "anomalous",
"data_complete_through": 1785081600,
"computed_at": 1785081900
}
Use a deterministic measurement ID and select one result per window. Keep model artifacts, scheduling, and recovery state outside GraphJSON.
A supported Single Line visualization can alert on deviation_score,
observed, or another numeric field. Also monitor whether the evaluator
continues to emit results.
Follow Derived events and scheduled rollups for versioning and restatement.
Evaluate the detector#
Before relying on it:
- replay known incidents
- inject missing and zero buckets
- test low denominators
- test a holiday and a planned release
- measure false positives and missed incidents
- verify the investigation is actionable
- exercise evaluator failure
- confirm alert recovery behavior
Review detectors after product growth, instrumentation changes, and seasonal shifts. A baseline is a versioned operational definition.
Launch checklist#
- The metric, grain, population, unit, and owner are explicit.
- Fixed thresholds are preferred where a real boundary exists.
- Baseline periods are comparable and exclude the observed point.
- Missing, zero, and incomplete states remain distinct.
- Ratios retain numerator and denominator.
- Seasonality, holidays, releases, and backfills are visible.
- Statistical scores are not mislabeled as probabilities.
- The evaluator has a deterministic version and freshness monitor.
- Alerts have an actionable response and independent paging where needed.
- Detector quality is reviewed against known incidents.
Use Targets, forecasts, and plan-versus-actual reporting when the question is whether performance met an approved plan or forward estimate. A target miss and an anomaly are different analytical states.
After a detector finds a meaningful movement, use Metric-change decomposition and root-cause analysis to reproduce the top line, rule out measurement defects, quantify segment contributions, separate mix shifts, and connect the result to evidence.