Do Not Sum Daily Unique Users into Weekly Active Users

Monday has two active accounts. Tuesday has two. Wednesday has one. That does not tell you whether the three-day window contains two, three, four, or five distinct accounts. The missing information is overlap: did the same accounts return on different days?

Weekly active users require deduplication over the whole weekly window, not addition of daily distinct counts. First fix the identity key, qualifying behavior, environment, timezone, and window. Then take the union of qualifying identities. A daily-count-only export generally cannot recover the exact union.

This is a tutorial about a custom metric, not a reproduction of any vendor's default dashboard. Google's user metric documentation distinguishes total, active, new, and returning users; those labels are not interchangeable. Our example counts signed-in accounts that perform one explicitly selected action, not people inferred across devices.

1. Write the aggregation contract before the query

For this fixture, an active account performs report_opened in project demo, environment production, with a non-empty string account ID. A heartbeat does not qualify. Test events do not qualify. Anonymous events are reported separately and are not guessed into account identities.

The sample window is Monday October 12, 2026 at 00:00 UTC through Thursday October 15 at 00:00 UTC, end excluded. It is a three-day teaching fixture, not a complete weekly report. The same union operation applies to a declared seven-day window. UTC is a deliberate simplification; use explicit local calendar boundaries if your business reports in another timezone.

The MDN Date reference explains the distinction between timestamp values and local-time methods. A timestamp and a reporting calendar are separate decisions. Store enough information to reproduce the boundary instead of inheriting the machine's local timezone accidentally.

2. Run a synthetic counterexample

Save the following as weekly-users.cjs and run node weekly-users.cjs with Node.js 20 or later. No SDK, account, database, or network connection is needed. Every account and event below is synthetic.

const assert = require('node:assert/strict');
const start = Date.parse('2026-10-12T00:00:00Z');
const end = Date.parse('2026-10-15T00:00:00Z');
const row = (day, account, extra = {}) => ({
  project: 'demo', env: 'production', event: 'report_opened',
  at: `2026-10-${day}T12:00:00Z`, account, ...extra
});
const events = [
  row('12', 'a'), row('12', 'b'), row('12', 'a'),
  row('13', 'a'), row('13', 'c'), row('14', 'a'),
  row('14', 'test-only', {env: 'test'}),
  row('14', 'heartbeat-only', {event: 'heartbeat'}),
  row('14', ''), row('14', 'other-project', {project: 'other'}),
  row('15', 'end-boundary', {at: '2026-10-15T00:00:00Z'})
];
function qualifies(e) {
  const t = Date.parse(e.at);
  return e.project === 'demo' && e.env === 'production' &&
    e.event === 'report_opened' && typeof e.account === 'string' &&
    e.account.trim() !== '' && Number.isFinite(t) && t >= start && t < end;
}
const selected = events.filter(qualifies);
const daily = new Map();
for (const e of selected) {
  const day = new Date(e.at).toISOString().slice(0, 10);
  if (!daily.has(day)) daily.set(day, new Set());
  daily.get(day).add(e.account);
}
const union = new Set(selected.map(e => e.account));
const counts = [...daily.values()].map(s => s.size);
assert.deepEqual(counts, [2, 2, 1]);
assert.equal(counts.reduce((a, b) => a + b, 0), 5);
assert.equal(union.size, 3);
assert.equal(selected.length, 6);
assert.equal(qualifies(events.at(-1)), false);
assert.equal(qualifies(row('12', 'a', {at: '2026-10-12T00:00:00Z'})), true);
assert.equal(qualifies(row('12', 'a', {at: 'not-a-date'})), false);
assert.equal(qualifies(row('12', null)), false);
const uniqueAcrossDays = days => new Set(days.flat()).size;
assert.equal(uniqueAcrossDays([['a', 'b'], ['a', 'b']]), 2);
assert.equal(uniqueAcrossDays([['a', 'b'], ['c', 'd']]), 4);
assert.equal(uniqueAcrossDays([]), 0);
console.log('PASS: 11 synthetic checks; daily sum=5, window unique=3');

The three useful numbers are deliberately different: six qualifying event rows, five account-days, and three distinct accounts in the window. Repeating an action on Monday increases event rows, not Monday's unique accounts. Returning on Tuesday adds an account-day, not necessarily another distinct account in the whole window.

MDN Set documents uniqueness within a collection; Array.filter defines row selection. The Node assertion API makes the expected differences executable rather than relying on a dashboard screenshot.

3. Know what daily summaries have destroyed

Two days can both show two unique accounts. In one dataset both days contain a and b, producing a union of two. In another, the second day contains c and d, producing a union of four. The daily summaries are identical; the exact period totals are not.

Daily cardinalities do not preserve cross-day membership. If you keep only those counts, neither summing nor averaging restores exact weekly users. Retain a suitably protected identity-level dataset or an appropriate mergeable distinct-count representation. Approximate sketches require an explicitly accepted error model; this example does not implement one.

For the same scope and a complete partition into days, the largest daily count is a lower bound and their sum is an upper bound on the union. These bounds stop being a valid comparison if one chart counts devices and the other accounts, or if the environment and event rules differ.

4. Turn this into a dashboard acceptance test

  1. Freeze a small fixture with repeated users across days, not just one-off users.
  2. State whether the label means accounts, devices, or resolved people. Do not silently mix them.
  3. Test both start inclusion and end exclusion, including your actual reporting timezone.
  4. Check event rows, account-days, and period-unique accounts separately.
  5. Document late-arrival and identity-remapping behavior before comparing historical snapshots.

For a self-hosted event pipeline such as SensorFlow, raw records make custom metric checks possible, but the metric is still your contract to define and test. The SensorFlow deployment and verification guide distinguishes a license-free synthetic demo from real Sensors Data SDK payload ingestion, which requires a commercial license. This fixture neither exercises that ingestion path nor claims an automatic built-in weekly-user report.

Use the counterexample as a regression test before approving a weekly-active chart. A correct daily dashboard is not proof that its numbers can be added into a correct weekly one.

Disclosure: published by the SensorFlow project account. AI assisted drafting and code review; the synthetic program was executed locally before publication. It is not customer data or a production benchmark.

评论

此博客中的热门博文

Choosing an Event Analytics Stack Without a Feature Checklist

Event Tracking QA: Why Signup Funnel Numbers Disagree

Event Count Is Not User Count: Define the Metric First