Privacy, Consent & Retention

The Web Analytics module ships with privacy defaults that make GDPR / CCPA / DACH / CalOPPA compliance achievable without piling on extra middleware. Tracking opt-outs are honored at three layers (client, network, server), IP addresses never reach storage in plaintext, and retention is configurable per tenant or per site.

Do Not Track (DNT) and Global Privacy Control (GPC)

The tracker reads navigator.doNotTrack and navigator.globalPrivacyControl on every event and includes both in the event's consent block:

consent: { analytics: null, marketing: null, dnt: true, gpc: false }

When the Site's respectDnt flag is true (the default), the server-side applySiteGuards step filters out any event whose consent.dnt === true or consent.gpc === true before it's queued for BigQuery insert. The /collect response reports those events under filtered.

The client also short-circuits earlier: when respectDnt is true AND DNT/GPC is detected, the tracker doesn't even enqueue the event — saving the network round-trip. The server-side filter exists as a defense-in-depth in case the client config was wrong or stale.

To opt out of respecting DNT (uncommon, regulated industries): set respectDnt: false on the Site. The tracker will then enqueue events even from DNT-signaling browsers. The Tenant is responsible for whatever legal basis allows this.

Consent API

For sites that use a cookie banner, the tracker provides an explicit consent API:

karma('consent', { analytics: true, marketing: false });

This updates the tracker's internal consent state. Every subsequent event includes the current values in its consent block. The server doesn't enforce anything on consent.analytics / consent.marketing — those are advisory fields you can filter on yourself in queries:

-- Count events with positive analytics consent
SELECT COUNT(*) FROM events
WHERE JSON_VALUE(consent_state, '$.analytics') = 'true'

The reason consent.analytics and consent.marketing aren't server-enforced: every site has different legal interpretations of what counts as "analytics" consent vs "essential cookies", and some sites need to record the un-consented event for legal-basis tracking ("we received this signal, we noted the lack of consent, we didn't act on it"). Recording the state per-event preserves that flexibility.

DNT/GPC are server-enforced because those are user-asserted preferences from the browser, not site-asserted ones.

IP Addresses

IPs are hashed before storage:

ip_hash = sha256(ip + tenant_salt).slice(0, 32);

Geo enrichment (country / region / city columns in BQ) is derived from the IP before hashing, using MaxMind's GeoLite2 database via the geoip-lite package — entirely in-process, no external API calls, no per-event cost. The raw IP is discarded immediately after the geo lookup and the hash are computed. By default only country + region are recorded; city-level geo is opt-in per Site (Site.collectCityGeo = true) since some jurisdictions consider city-level location PII. Private and loopback IPs (RFC1918, IPv6 ULA, link-local) are not resolved — they yield all-null geo so dev / staging / internal-proxy traffic isn't mis-attributed.

The GeoLite2 data is bundled with the geoip-lite package and updates on npm install. Redeploy roughly monthly to keep country / region accuracy current.

To delete all events from a single IP retroactively (e.g. on a deletion request), compute the same hash with the tenant salt and run:

DELETE FROM events
WHERE tenant_id = @tenant
  AND ip_hash   = @hash

Right-to-be-forgotten requests for a known user_id are simpler — both events.user_id and identity_map.user_id are deletable directly.

Retention

Default retention is 25 months (Tenant.analyticsRetentionDays = 760), which is longer than GA4's 14-month cap and intentionally chosen to be long enough for year-over-year analyses.

Per-Tenant Override

Set Tenant.analyticsRetentionDays to any number of days, or to null for "unlimited" (paid tier).

Per-Site Override

Site.analyticsRetentionDays overrides the tenant default for one site. Useful for sites with stricter privacy requirements than the rest of the tenant's portfolio.

How Retention Is Enforced

A nightly Cloud Run Job (analyticsRetentionJob.js, triggered via the ticks system) runs:

DELETE FROM events
WHERE tenant_id = @tenant
  AND site_id   = @site
  AND event_ts  < TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL @days DAY)

per site whose retention is finite. The same is done for identity_map and any rollup collections.

The job logs deletion counts to the platform log so you have an audit trail of how much was removed.

Tenant Deletion

When a Tenant is being offboarded, deletion is a single one-line query against the shared dataset:

DELETE FROM events           WHERE tenant_id = @tenant;
DELETE FROM identity_map     WHERE tenant_id = @tenant;
DELETE FROM events_dlq       WHERE tenant_id = @tenant;

Plus dropping the tenant's Mongo database. The fleet bootstrap script has a --delete-tenant flag that runs all of this in order; never run those DELETEs by hand from a SQL console because forgetting the WHERE tenant_id = ... would wipe everyone.

What's Not Tracked

Cookie Banner Integration Recipes

OneTrust

// Inside your OneTrust group toggle handler:
function onOneTrustChange() {
  const analyticsConsent = OnetrustActiveGroups.includes('C0002'); // Performance
  const marketingConsent = OnetrustActiveGroups.includes('C0004'); // Marketing
  karma('consent', { analytics: analyticsConsent, marketing: marketingConsent });
}

CookieYes / Cookiebot / Generic

// On any consent-changed event:
karma('consent', {
  analytics: consentApi.has('analytics'),
  marketing: consentApi.has('marketing'),
});

"Consent is given by default, opt-out via UI"

// On script load, default to opted-in:
karma('consent', { analytics: true, marketing: true });
// If user later opts out:
document.querySelector('#opt-out').addEventListener('click', () => {
  karma('consent', { analytics: false, marketing: false });
});

What the Tenant Has to Display

Most privacy frameworks require some disclosure in your privacy policy. A typical paragraph:

We use KarmaFlow Web Analytics, a first-party analytics platform, to understand how visitors use our site. We collect page views, clicks, form submissions, scroll depth, and performance metrics. IP addresses are hashed before storage. We honor Do Not Track and Global Privacy Control browser signals. You can configure your preferences via the cookie banner above.

If you've turned respectDnt off, document that explicitly; many jurisdictions require disclosure of opt-out signals not being honored.