Gap
Segments (#23) can narrow a population before a count is taken, and funnels (#21) report per-step counts. Neither applies a minimum-population floor, so a sufficiently narrow segment reduces an "aggregate" report to the behaviour of one identifiable person.
Every filter in the chain is individually innocuous and genuinely aggregate. The disclosure is created by composing them.
Reproduction
Seeded a modest site — 61 visitors, mostly US/desktop, plus exactly one from Iceland on a tablet — and narrowed with the live filter engine:
-- the site as a whole --
visitors matching no filter: 61
funnel: Viewed pricing=31 -> Started checkout=11
-- narrowing, one innocuous filter at a time --
country = IS visitors = 1
+ device = tablet visitors = 1
-- and the funnel for that segment --
segment population: 1 visitor(s)
Viewed pricing 1
Started checkout 1Two filters. The result is a complete behavioural timeline for one person, served from endpoints whose entire promise is that they are aggregate.
Note the funnel results endpoint does not accept filters today, so the last block models what applying a saved segment to it would produce. That is the obvious next feature request, which is precisely why the floor should be decided before it ships rather than after.
Why this is ours to care about
This is not the usual k-anonymity setting, where the concern is published data. The dashboard is authenticated and scoped to sites you own or are a member of — you are looking at your own site's visitors.
It matters anyway, because the product's claim is that nobody can identify an individual visitor, including the site owner. That is the whole basis for being cookieless and consent-free: there is no individual to consent about. A report that reconstructs one person's journey contradicts the claim the tracking script's existence rests on.
It also has teeth in the multi-tenant case shipped in #19: a viewer on a shared site is not necessarily someone the owner intended to hand individual-level behaviour to.
Related: the public widget surface (#26) side-steps this entirely by exposing no dimensions and no filters. That was the right answer there — remove the capability rather than guard it — but it is not available here, because segmenting is the feature.
Options
A. k-anonymity floor on segmented results. Suppress any cell whose population is below k (≈5), applied only when a filter or segment is active. Unsegmented totals are never suppressed — "you had 3 visitors yesterday" identifies nobody, and suppressing it would make the product look broken on day one.
- honest, standard, and the strictest option
- needs a decision about what a suppressed cell renders as, and that is a UX question as much as a privacy one
B. Minimum segment population. Refuse to apply a segment matching fewer than k identities at all, with a clear message. Fails earlier and is far easier to explain than a scatter of suppressed cells.
- one check, one message, one place
- coarser: a segment just over the line still yields near-unique cells downstream
C. Decide the risk is acceptable and write down why. Defensible for a single-owner install; much weaker once #19 memberships and shared dashboards are in play.
Recommendation
B for now, A later if the reports get richer. B is a single check at the point a segment is resolved, it explains itself, and it does not require designing a suppressed-cell state across every chart. A is the more complete answer but pays for that with UX surface in a dozen places.
Either way, k belongs in config/privacy.ts next to retentionDays and respectDnt, so an operator can raise it, and the default should be stated in the privacy documentation rather than buried.
Why this is not already implemented
Turning any of this on changes what every low-traffic filtered view returns. A new site would find most segmented views empty on the first day, which reads as a broken product rather than a working privacy guarantee. That is a product decision, not something to slip in as part of a feature commit — which is why #23 and #21 shipped without it and this exists instead.
Not blocking
Nothing shipped is unsafe today: the funnel endpoint takes no filters, so the composition above cannot currently be performed through the API in one call. This should be settled before funnels accept segments.