Event Count Is Not User Count: Define the Metric First
An analytics report can show 240 events and 80 users without a contradiction. One metric counts qualifying event records; the other counts distinct identifiers under a chosen identity rule.
Before comparing dashboards, define the event filter, duplicate-delivery rule, time window, and identity key. This tutorial uses a synthetic signup fixture and a short Node.js program. It does not use customer data or claim to reproduce any vendor’s identity graph.
I work on SensorFlow. This article is an independent measurement example, not a claim that SensorFlow automatically resolves people or merges anonymous IDs. The sample records are fictional.
Four questions belong in the metric definition
“Signup started” might mean a form was opened, a submit action was attempted, or a backend accepted a registration. Pick one business fact and name the event accordingly. A button click is not proof that a signup succeeded.
Then state the scope:
- Event count: how many qualifying, deduplicated records occurred in the interval?
- Identifier count: how many distinct
distinct_idvalues occur among those records? - Person count: what verified identity-resolution rule maps identifiers to people?
- Conversion rate: which eligible starts and completions belong to the same cohort and time window?
The last two require additional definitions. A distinct_id is a counting key, not guaranteed to equal one human. Shared devices, anonymous-to-login transitions, multiple accounts, and identifier resets can all change the relationship between IDs and people.
A runnable fixture: same event, different counts
Save this as event-count.mjs and run node event-count.mjs. It uses only Node’s built-in assertion module.
import assert from 'node:assert/strict';
const rows = [
{ project: 'demo', id: 'e1', distinct_id: 'anon-a', event: 'SignupStarted' },
{ project: 'demo', id: 'e2', distinct_id: 'anon-a', event: 'SignupStarted' },
{ project: 'demo', id: 'e3', distinct_id: 'anon-b', event: 'SignupStarted' },
{ project: 'demo', id: 'e4', distinct_id: 'acct-a', event: 'SignupCompleted' },
{ project: 'demo', id: 'e5', distinct_id: 'anon-b', event: 'SignupStarted' },
{ project: 'demo', id: 'e6', distinct_id: 'acct-b', event: 'SignupCompleted' },
{ project: 'demo', id: 'e4', distinct_id: 'acct-a', event: 'SignupCompleted' },
{ project: 'demo', id: 'e7', distinct_id: 'acct-a', event: 'SignupCompleted' }
];
function metrics(input, eventName) {
const unique = new Map();
for (const row of input) {
for (const key of ['project', 'id', 'distinct_id', 'event']) {
assert.equal(typeof row[key], 'string', `${key} must be a string`);
assert.ok(row[key].length > 0, `${key} must not be empty`);
}
const key = JSON.stringify([row.project, row.id]);
const previous = unique.get(key);
if (previous) assert.deepEqual(previous, row, 'conflicting duplicate event key');
else unique.set(key, row);
}
const selected = [...unique.values()].filter(row => row.event === eventName);
return {
event_count: selected.length,
identifier_count: new Set(selected.map(row =>
JSON.stringify([row.project, row.distinct_id]))).size
};
}
assert.deepEqual(metrics(rows, 'SignupStarted'), {
event_count: 4, identifier_count: 2
});
assert.deepEqual(metrics(rows, 'SignupCompleted'), {
event_count: 3, identifier_count: 2
});
assert.deepEqual(metrics([], 'SignupCompleted'), {
event_count: 0, identifier_count: 0
});
assert.throws(() => metrics([...rows, {
...rows[0], distinct_id: 'different-id'
}], 'SignupStarted'));
assert.throws(() => metrics([{ ...rows[0], distinct_id: '' }], 'SignupStarted'));
console.log('PASS: 5 synthetic event-count checks');
The fixture has four SignupStarted deliveries, all with distinct event IDs, so deduplication leaves four events and two identifiers: anon-a started twice, while anon-b started twice. For SignupCompleted, four deliveries reduce to three unique events because e4 is a repeated delivery of the same completion record; the result is three events and two identifiers.
For SignupCompleted, there are three unique events and two identifiers. acct-a completed twice; acct-b completed once. The code does not infer that anon-a and acct-a are the same person. Without a verified identity transition and an explicit analysis rule, those IDs remain separate. The Sensors Data identity guide describes identity association as a deliberate setup and validation step; the fixture here does not model that feature.
Deduplication is part of the metric, too
This fixture treats (project, id) as the synthetic delivery key. A repeated key with identical fields counts once; the same key with different fields fails loudly. Real ingestion systems may use different event identifiers and retention semantics, so choose a key that matches the producer’s retry behavior and document the time horizon.
This check is in-memory and covers only the rows supplied to it. The program uses JavaScript's Map for the synthetic event key and Set for unique identifiers. A production query must also define late arrivals, time boundaries, project isolation, deletion or correction events, and whether deduplication spans partitions. Do not assume that the small map above provides exactly-once delivery.
Identity determines what “users” can mean
identifier_count is deliberately named that way. Counting IDs is reproducible; calling them people requires a justified identity model. If one person uses an anonymous ID and then signs in with an account ID, a report can show two identifiers until an approved identity-linking rule connects them.
Likewise, a shared tablet can produce one anonymous ID for several people. If an account is shared, one account ID can represent more than one person. The correct interpretation depends on product behavior and the identifiers collected, not on the COUNT(DISTINCT ...) syntax.
Before someone compares two dashboards
Write down the event name and filters, deduplication key, date interval and timezone, identity field, and cohort rule. Run the same synthetic cases through both reports. If the counts differ, inspect each definition before assuming one system lost data.
For a product analytics pipeline, SDK, receiver, storage, and report each have a separate verification point. An event count confirms records under a query definition; it does not prove unique people, successful account creation, or a correct conversion funnel.
SensorFlow is an open-source, self-hosted event data path that can receive compatible Sensors Data SDK traffic after a commercial license is activated. Its free deployment demo uses synthetic events. This code is an offline teaching fixture, not a built-in SensorFlow metric or an SDK end-to-end test. See the SensorFlow deployment and SDK integration guide for the actual setup boundary.
The Node.js v24.17.0 run passed five assertions over synthetic records. It tests the metric definitions shown here; it says nothing about real users, production traffic, or analytics platform performance.
This article was drafted with AI assistance and checked against official documentation and a local code run. I work on SensorFlow.
评论
发表评论