Theory
Metric definition: one rule, one number
How to write a metric definition so two people get the same number: qualifying event, unit, exclusions, time window and time zone, on a DAU example.
~10 min read · 9 exercises · 250 points
Before you start
Six or seven?
At Halves, two teammates bring you the same question with different answers. Maya's dashboard says Halves had six active users on Wednesday, 3 June; Theo's sheet says seven. Same app, same day, and only one number can go to the board.
In this lesson you'll find out why both can be "right", and write a metric definition precise enough that any two people who follow it get the same number.
A label is not a definition
Maya's dashboard says 6 active users on Wednesday 3 June; Theo's sheet says 7. The usual reflex is to hunt for the buggy query. Usually there isn't one.
"Active users on Wednesday" is a label. It sounds exact but leaves open every choice that moves the number:
- which action makes someone count: any event, or opening the app;
- what is counted: people, devices or rows;
- which accounts are left out, such as staff and test accounts;
- how long the window is and exactly when it starts;
- whose clock decides where Wednesday begins.
Maya counted anyone with any event on the UTC date. Theo counted app opens on each user's local date. Neither removed staff. Both queries are correct; they answer two different questions that share one name.

So when two numbers disagree, ask first what each one counted, not who made a mistake. And notice what a definition doesn't contain: the chart type, or whether the result is good. Those come after the number exists.
- Do“Maya's 6 counts any event by UTC date; Theo's 7 counts app opens by local date. Neither drops staff.”
- Don't“One of the two queries has a bug; let's take the bigger number.”
Assumes an error instead of comparing the definitions.
The five parts of a metric definition
A metric definition nobody can misread has five parts, and each one closes a trap from the Halves data.
- Qualifying action: the event that makes someone count. For "active",
app_opened, not any event, because server-side recurring bills would make people look active. - Unit counted: distinct users, groups or settlements. Settle-ups are logged by both sides, so settlements are distinct
settlement_id, not rows. - Exclusions: who is left out and how to find them:
is_internal = trueoracquisition_channel = staff, because one flag was left empty. - Time window: a day, a week or a month, with its exact start and end.
- Reporting time zone: whose midnight splits the days, UTC or the user's.
Written together, the definition of DAU for Halves reads: distinct user_id with app_opened, staff excluded by flag or channel, one calendar day 00:00–23:59, UTC.
The same template works for any product metric. If two people can follow your definition and get different numbers, one of the five parts is still missing.
- DoDAU:
app_opened· distinctuser_id· minus staff (flag or channel) · calendar day · UTC - Don'tDAU: “users who were active today”
Repeats the label; all five parts are still open.
Fixed windows, not moving ones
The time window says over which period the metric counts, and it needs an exact start and end, plus the time zone they're in.
A tempting shortcut is a window relative to now: "the last 7 days", "the past 24 hours". It's fine for a live monitor, but for a reported number it's a trap. The result depends on the moment someone runs the query. Run it on Thursday at 09:00 and at 17:00, and the windows differ by eight hours of data, so two people get two numbers, and nobody can reproduce last week's slide.
For reporting, use calendar windows: "Monday 1 June 00:00 to Sunday 7 June 23:59, UTC". Anyone, at any time, gets the same answer.
Two details to write down as well:
- where the week starts, Monday or Sunday; teams differ;
- whether the last period is complete. A week that ends tomorrow is still filling up, so its number will grow. Mark it as partial or leave it out of the chart.
- Do“WAU, Mon 1 June 00:00 – Sun 7 June 23:59 UTC: 17.”
- Don't“WAU (last 7 days): 17”, run on Thursday afternoon.
Nobody can rerun it and land on the same seven days.
Recounting a day in UTC
With the definition fixed, recounting Wednesday is mechanical. The steps, in order:
- Window and time zone first. In UTC, the date is exactly what
event_timesays, so keep rows from 2026-06-03T00:00 to 23:59. - Qualifying action. Keep only
app_opened. - Exclusions. Look up each
user_idin the users table and drop accounts withis_internal = trueoracquisition_channel = staff: u019 and u002. - Unit. Count distinct
user_id, so someone who opened the app twice counts once.
Dataset · 42 rows
Halves events, noon 2 June to noon 4 June (UTC)
42 events from the week's log. Times are UTC.
First 5 of 42 rows
On 3 June in UTC, six users opened the app: u011, u002, u019, u004, u018 and u021. The two staff accounts opened it twice each. Remove them and 4 real users remain.
The order matters less than not skipping a step, but filtering the window first keeps the list short. A useful self-check is to count the same thing two ways: distinct users before exclusions minus the excluded ones that appear (6 − 2) should equal the distinct users after exclusions (4).
- DoUTC 3 June ·
app_opened· minus u002, u019 · distinct: u011, u004, u018, u021 = 4 - Don'tCount
app_openedrows on 3 June UTC: 8.Counts the staff accounts' repeat opens as extra users.
Counting by the user's local date
A reporting time zone decides whose midnight splits the days. Counting by the user's local date suits questions about daily habits and country campaigns, and it takes one extra step.
For each event, add the user's UTC offset to event_time and take the date of the result. In June: New York is UTC−4, Lisbon UTC+1, Kyiv UTC+3, Bangkok UTC+7.
- u012 in Bangkok opens the app at 23:10 UTC on 2 June: 23:10 + 7 h = 06:10 on 3 June. In.
- u017 in New York opens it at 01:40 UTC on 4 June: 01:40 − 4 h = 21:40 on 3 June. In.
- u011 in New York opens it at 02:30 UTC on 3 June: that's 22:30 on 2 June. Out.

With the same exclusions as before, local 3 June has 5 real users: u004, u012, u017, u018, u021. Check from the UTC count: 4 − 1 + 2 = 5.
Use the offset that applies on that date: summer time shifts many zones by an hour, so a fixed offset table must name its season.
- Dou012, Bangkok: 23:10 UTC 2 June + 7 h = 06:10 local, 3 June → counts.
- Don'tu012, Bangkok: 23:10 UTC 2 June − 7 h = 16:10, 2 June → doesn't count.
Subtracts the offset instead of adding it.
Reproduce a number before you fix it
When two dashboards disagree, the fastest way to end the argument is to reproduce each number from the raw data using the rule its author applied. If you land on their number, the data is fine and only the definitions differ.
For Maya's 6: take every row dated 2026-06-03 in UTC, whatever the event and whoever the user. There are 16 rows. Count distinct user_id among them: 6. That is her dashboard, exactly.
Dataset: Halves events, noon 2 June to noon 4 June (UTC) · shown above
For Theo's 7: take app_opened, shift each time to the user's local clock, keep local 3 June and count distinct users, staff included: 7.
Now you can show the gap step by step instead of declaring a winner: "6 = any event, UTC, staff in; dropping staff gives 4. 7 = app opens, local date, staff in; dropping staff gives 5."
This also catches the rare real bug. If you follow someone's stated rule and can't reproduce their number, then something in their query differs from what they think it does, and that's worth fixing.
- Do“Your 6 is any event by UTC date with staff in: 16 rows, 6 users. With our rule it's 4.”
- Don't“Your dashboard is wrong; the real number is 4.”
Her dashboard is right for its own rule; say which rule you're using instead.
One week, four numbers
Stretch the same thinking from a day to the week of 1–7 June (UTC) and you get a ladder of numbers, each one a different definition:
- 22: distinct users with any event, nobody excluded;
- 19: only
app_opened, which drops the 3 users whose only events were recurring bills created by the server; - 18: also excluding
is_internal = true, which removes u019; - 17: excluding
is_internal = trueoracquisition_channel = staff, which also removes u002, whose flag was left empty.
Only 17 is the agreed DAU rule stretched to a week. The others aren't wrong numbers; they're answers to other questions, such as "how many accounts did anything at all?"
Two habits follow. First, when you see several versions of one metric, line them up like this and name the step that separates each pair; the gaps tell you what each team forgot. Second, put the definition under the chart as a caption. Then 17 on one slide and 22 on another can't both claim to be "weekly active users".
- Do“22 any event → 19 app opened → 18 minus flag → 17 minus all staff.”
- Don'tAverage the four versions and report 19 WAU.
Averaging definitions gives a number nobody can define.
Changing a definition without breaking the chart
Definitions do change, and they should when a better rule appears: a new staff signal, a switch to local dates, a better qualifying action. The danger is the quiet change.
If DAU is counted by UTC dates until 30 June and by local dates from 1 July, the year's chart joins two different metrics. A jump at the switch may come from the rule, not from users, and readers can't tell.
Three safe ways to change a definition:
- Recompute history with the new rule, so the whole chart uses one definition.
- If you can't, mark the break on the chart and in the caption ("local dates from 1 July") and never compare across it.
- For a while, report both versions side by side, so people see how much the rule alone moves the number.
In every case keep a dated changelog in the definition: what changed, when and why.
How it fits together: a number is its definition plus that definition's history: five parts, a fixed window, and a rule anyone can reproduce. Next, A tracking plan for a new feature designs the events those definitions need.
- Do“DAU recomputed back to January with local dates (changed 1 July: travel-heavy users).”
- Don'tSwitch the query on 1 July and let the chart continue as one line.
Readers will take the step at the switch for a change in users.
Ready to practise?
Answer the lesson's exercises and earn 250 points the first time you finish it.
Start the exercises