Перейти к основному содержимому

Status: on file

Covers analytics.events + analytics.sessions — the anonymous lane, keyed by SHA256(ip:ua). The authenticated lane (analytics.user_events) rests on Art 6(1)(b)/(f) and is assessed separately in two-lane-model.md.

Controller: the site operator (Germany). Basis claimed: GDPR Art 6(1)(f).

Why this document exists

Two things are easy to get wrong here, and both were wrong before this assessment:

  1. The hash does not make the data anonymous. In EDPS v SRB (C‑413/23 P, 4 Sept 2025) the CJEU held that identifiability is judged from the controller's own position and at the time of collection. We hold the raw IP and User-Agent before hashing and we control the algorithm. The result is therefore pseudonymous personal data for us, whatever it looks like to a third party. GDPR applies in full, and a legal basis is required.
  2. ePrivacy and GDPR are separate gates. Clearing one does not clear the other. This document addresses only the GDPR limb. The terminal-equipment question is in two-lane-model.md.

Per EDPB Guidelines 1/2024 (adopted 8 Oct 2024, reflecting CJEU C‑621/22 KNLTB), Art 6(1)(f) requires a cumulative three-part test. All three are assessed below.


Limb 1 — Purpose test

Interest: understanding how the site is used, in aggregate, in order to operate and improve it: which pages are read, which are ignored, where navigation breaks down, where the interface is slow, and where visitors get stuck.

Is the interest real (not speculative), present (not hypothetical), and lawful?

  • Real and present: the data feeds live surfaces (/admin/analytics, the analytics showcase pages) that inform actual decisions about what to build and fix.
  • Lawful: KNLTB confirmed that a purely commercial interest can qualify. Operating and improving one's own website is well within that.

A purely commercial interest still has to survive Limb 3. It is not a free pass.

Limb 2 — Necessity test

The bar is necessary, not useful and not more convenient. For each element we ask whether a less intrusive route reaches the same purpose.

Element Necessary? Reasoning
Keyed visitor hash Yes Without any grouping key, pageviews cannot be distinguished from reloads and "unique visitors" is unanswerable. It is HMAC-SHA256(server key, ip + user-agent) — keyed, so it cannot be reversed by sweeping the address space, which an unkeyed digest can be. It does not rotate daily: "unique visitors over 30 days" is a question the hash exists to answer, and a daily-rotating input would inflate that count by the number of days. The key is rotatable instead, at or above the 60-day retention window, so at most one boundary can fall inside any reporting range.
Path + templated route Yes The measurement itself.
Country (from connection) Yes Answers "who is this site for", and is coarse by construction — country level only, never city or coordinates.
Referrer Yes Answers "how do people find this", unanswerable any other way.
Device / browser family Yes Drives what gets tested and supported. Deliberately coarse: family only, never a version string.
Engaged time, scroll depth Yes Distinguishes read from bounced. Wall-clock time cannot — a forgotten tab looks identical to careful reading.
Rage / dead clicks, form-field abandonment Yes The "where do people get stuck" question. These are the aggregate signals that make session replay unnecessary, which is the less-intrusive-alternative argument in its strongest form.
Web Vitals + attribution Yes A bare metric identifies a problem; attribution identifies the element responsible. Without it the data cannot be acted on, so collecting it without attribution would fail this limb.

Rejected as unnecessary — considered and deliberately not collected:

  • Session replay / DOM recording. Would answer some of the same questions far more intrusively. The derived signals above cover the purpose. Fails Limb 2 outright.
  • Heatmaps, mouse tracks, keystrokes. Same reasoning.
  • Form field content. Only which field was abandoned is needed. Content may incidentally capture Art 9 special-category data, and is never read.
  • Any additional fingerprinting entropy — screen size, timezone, canvas, fonts, hardwareConcurrency. Would marginally improve visitor counting at a disproportionate cost to identifiability. Also see the ePrivacy consequence in the two-lane doc: adding entropy moves the technique into the confirmed scope of EDPB Guidelines 2/2023.
  • Raw IP storage. Never stored in any table. The hash is computed and the input discarded.
  • Cross-site or third-party identifiers. No data leaves the first party. There is no vendor, no pixel, no tag manager.

Limb 3 — Balancing test

Nature of the data: pseudonymous, no special categories, no children's data sought.

Reasonable expectations (Recital 47). A visitor to a website reasonably expects the operator to count visits and measure performance. That is the mainstream expectation for first-party analytics. Where expectation gets strained is tracking across sites, profiling, and ad targeting — none of which happen here.

Impact on the data subject: low, and bounded by design.

  • No decision is ever made about an individual. Output is aggregate.
  • No profile is built: the identifier rotates every UTC day, so nothing accumulates across days even in principle.
  • No cross-site tracking, no data sharing, no sale.
  • Raw events are deleted after 60 days; aggregates carry no identifier at all.

Safeguards applied (each is enforced in code, not policy):

Safeguard Where
Keyed, non-reversible visitor identifier analytics/consent.ts hashVisitorId, keyed via analytics/visitor.ts
Daily-rotating session identifier analytics/consent.ts deriveCookielessSessionId
Referrer reduced to an origin before storage db/analytics/mutations.ts — the write chokepoint, so every lane inherits it
No raw IP persisted anywhere analytics/hook.ts — hashed before write
Authenticated surfaces excluded from this lane analytics/collect-policy.ts
No join key to the identified lane schema-level; see user-events.ts docblock
Closed allowlist on event names and properties analytics/event-schema.ts
60-day deletion, pinned by test jobs/analytics-cleanup.ts, analytics.test.ts
Bot and prefetch traffic excluded analytics/collect-policy.ts

Right to object (Art 21). Rejecting the analytics tier in the consent banner stops the cookie, the referrer, device/browser parsing, and all behavioural events. The banner is reachable at any time from /showcases/privacy/cookies, and rejection is exactly as easy as acceptance (equal-weight buttons, no dark pattern — EDPB Guidelines 05/2020 and the 2023 Cookie Banner Taskforce report).

Outcome: the interest is not overridden. The processing is proportionate, the impact is low and time-boxed, an objection route exists and is honoured in code, and every genuinely intrusive alternative was considered and rejected.

Review triggers

Re-run this assessment before any of the following. Each would change a limb:

  • Adding any signal to the visitor hash (Limb 2 and Limb 3 both change).
  • Rotating the visitor-hash key faster than the retention window, or not at all for longer than one.
  • Widening the stored referrer beyond an origin. The full URL of a page a visitor arrives from can contain a magic-link, password-reset or OAuth token in its query string.
  • Introducing any cross-session or cross-site identifier.
  • Extending raw-event retention beyond 60 days.
  • Introducing any per-individual decision, ranking, or personalisation from this lane.
  • Sharing analytics data with any third party.
  • Adding session replay, heatmaps, or form-content capture — each was rejected at Limb 2, so reintroducing one reopens the whole assessment.
← Back to Blueprint

Думаете, этот паттерн можно сделать лучше? Расскажите как.

Оставить отзыв