Datar Aggregate Analytics
Not every team that runs feature flags has a data pipeline. Sometimes you just want to know: how many evaluations did this flag get this week, split by variant and segment? Standing up Kafka, a consumer, and a warehouse to answer that question is overkill. Datar is the answer for that case — an optional in-memory aggregate analytics engine built into Flagr. It tallies evaluation counts by flag, variant, segment, and hour, then exposes the results through two REST endpoints. No external pipeline, no Kafka consumer, no separate analytics stack — just one more entry in FLAGR_RECORDER_TYPE.
When to use
Datar exists in the gap between "no analytics" and "full pipeline." If you already run Kafka and a warehouse, you don't need it — your streaming recorders give you richer data. Datar is for the team that wants a quick dashboard without standing up infrastructure: basic evaluation counts broken down by variant and segment, queryable through a REST endpoint, persisted in a single table.
Prometheus covers rate-based metrics and variant-level time-series well, but it cannot index by segment_id due to high cardinality. Use Datar when you need:
- Segment breakdowns — how many evaluations each segment received
- Historic totals — cumulative counts (not just rates) over days or weeks
- Per-flag dashboards — a simple summary view across all flags without a separate analytics stack
For client-reported impressions and warehouse-style A/B analysis, use Exposure logging and Data recorders & A/B analysis (Kafka, Kinesis, or Pub/Sub). Datar does not ingest exposure rows.
Enabling
Datar runs under the same master switch as every other recorder. Turn that switch on with FLAGR_RECORDER_ENABLED=true, then add datar to the FLAGR_RECORDER_TYPE list to activate the in-memory aggregator:
export FLAGR_RECORDER_ENABLED=true
export FLAGR_RECORDER_TYPE=kafka,datar
export FLAGR_RECORDER_DATAR_FLUSH_INTERVAL=60s # defaultThe datar_hourly_events table is created automatically by AutoMigrate on startup. No schema migration is needed.
Recording
Datar shares the same recording gates as the streaming recorders described in Recording gates, with one key difference: it counts evaluations only. Rows carrying recordSource: exposure from POST /exposures are skipped, so impression-based experiments never reach Datar's counters.
The per-flag dataRecordsEnabled setting still applies, and you toggle it through PUT /api/v1/flags/{id} as usual. A flag must opt in before its evaluations are counted.
Note: After creating or updating a flag, wait for EvalCache to reload before evaluations count — EvalCache freshness.
Endpoints
Datar exposes two endpoints: a fleet-wide summary that shows which flags saw traffic, and a per-flag breakdown that splits that traffic by variant, segment, and day.
GET /api/v1/datar/summary
Returns flags with aggregate totals over a time window. Only flags that have actual evaluation traffic in the window appear — zero-traffic flags are excluded.
| Param | Type | Default | Description |
|---|---|---|---|
from | RFC 3339 | 7 days ago | Start of time window |
to | RFC 3339 | now | End of time window |
limit | int | 100 | Max results |
offset | int | 0 | Result offset |
Response:
{
"flags": [
{
"flagID": 1,
"flagKey": "my-feature",
"enabled": true,
"description": "Controls feature X",
"totalEvalCount": 45283,
"lastEvaluatedAt": "2026-05-22T14:30:00Z"
}
]
}GET /api/v1/datar/flags/{flagID}/summary
Detailed breakdown for a single flag. Returns traffic grouped by variant, segment, and day — all three arrays sorted (descending by count for variant/segment, ascending by date for day).
| Param | Type | Default | Description |
|---|---|---|---|
from | RFC 3339 | 7 days ago | Start of time window |
to | RFC 3339 | now | End of time window |
Response:
{
"flagID": 1,
"trafficByVariant": [
{ "variantID": 1, "count": 30188 },
{ "variantID": 2, "count": 15095 }
],
"trafficBySegment": [
{ "segmentID": 10, "count": 30188 },
{ "segmentID": 20, "count": 15095 }
],
"trafficByDay": [
{ "date": "2026-05-21", "count": 22100 },
{ "date": "2026-05-22", "count": 23183 }
]
}Note:
trafficBySegmentexcludes rows withsegment_id = 0(the no-segment-match bucket). Evaluations that matched no segment are still counted intrafficByVariantandtrafficByDay, but do not appear in the per-segment breakdown.
Data model
What Datar stores follows directly from what it counts. There are no per-user rows, no unique-entity counting, no event payloads. Each evaluation bumps an in-memory counter keyed by (flag, variant, segment, hour), and a background goroutine flushes those counters to one table periodically. The trade-off is that you lose entity-level detail — you can't ask "which users saw this variant" — but you gain a tiny, fast, zero-dependency store that runs alongside evaluation without measurable cost.
Counts are bucketed by hour using time.Now().Truncate(time.Hour). Each row in the datar_hourly_events table represents one unique combination of:
flag_id— the evaluated flagbucket_hour— the truncated hour timestampvariant_id— the matched variantsegment_id— the matched segment (0if no segment matched)
A unique composite index on (flag_id, bucket_hour, variant_id, segment_id) ensures additive UPSERTs work correctly across concurrent instances.
Resource usage
The design choices above show up here as the cost of running Datar. The hot path uses sync.Map with atomic increments, which means zero allocations on the existing-key path, and the database sees one batch transaction per flush interval — nothing more.
Note: The numbers below are indicative, not benchmark-verified — the repo currently has no Datar-specific benchmark. Measure on your own hardware before sizing capacity.
- CPU: ~87ns per evaluation on the hot path (existing key), ~98ns for new keys; zero allocations.
- RAM: ~210 bytes per active (flag, variant, segment) tuple; ~2.1 MB for 10K keys.
- DB writes: One batch transaction every flush interval (configurable, default 60s).
- Table growth: ~2.4K rows/month per 100 flags (hourly buckets, no retention).
Limitations
Datar's simplicity comes from what it doesn't do, and knowing those boundaries tells you when to reach for a streaming recorder instead:
- Crash loss — data is in-memory until the periodic flush. If the process crashes, up to one flush interval of aggregate data is lost (acceptable for dashboard analytics).
- No retention policy — the table grows unbounded. Deploy a cron job or retention policy if needed.
- No unique entity counting — each evaluation is counted once regardless of entity identity (no HyperLogLog or similar).
- Evaluations only — rows with
recordSource: exposurefromPOST /exposuresare not counted. Use a streaming recorder (Kafka, Kinesis, or Pub/Sub) and Data recorders & A/B analysis for impression-based experiments.
