Privacy & compliance

Canopy's privacy story is not a policy layered on top of tracking — it is the architecture. The tracker stores nothing in the browser, sends nothing to third parties, and the server never persists a raw IP address. This page documents exactly what is collected, what is engineered out, and what that means for GDPR and consent banners. It is documentation, not legal advice — you own your compliance posture.

1What is collected

Every event stores exactly these fields — no more:

FieldPurposeNotes
Event nameWhat happenedpageview, outbound, identify, or your custom events.
URL, path, titleWhich pageCaptured from the browser at send time.
ReferrerWhere the visitor came fromEmpty for direct traffic.
Viewport widthDevice classMobile vs desktop — no full device profile.
Session IDGrouping events into visitsA random in-memory UUID — see below.
IP hashDistinguishing visitorsSalted SHA-256, truncated to 16 hex chars.
CountryRough geographyFrom CDN headers (Vercel/Cloudflare) when present.
PropertiesYour custom dataWhatever your events attach, stored as JSONB.

2What is engineered out

Never collectedHow
CookiesThe tracker writes nothing to the browser — no cookies, no localStorage, no IndexedDB.
Raw IP addressesThe server hashes the IP with SHA-256 and a secret salt, keeps 16 hex chars, and discards the original.
FingerprintsNo canvas, audio, font, or device fingerprinting — the data to build one is never gathered.
Cross-site trackingEvents post to your domain about your pages. Canopy cannot see visitors on other sites.
Personal details by defaultNo names, emails, or accounts — unless you explicitly send them in identify or event props.

3The session model

When the first event of a visit is sent, the tracker generates a random UUID and keeps it in memory. Every event in that page's lifetime carries the same session ID, so SPA navigations group into one visit. When the tab closes, the session is gone — nothing was ever written down.

session lifetimein-memory only
page load ──► pageview ──► SPA route ──► pageview ──► tab closed
                 └──────────── same session ID ────────────┘   ✕ gone

This is why Canopy needs no consent banner in most jurisdictions: there is no storage to consent to, and no identifier that outlives the page. On logout, canopy.reset() discards the session so the next activity starts fresh.

4IP hashing in detail

The ingestion endpoint extracts the visitor IP from standard proxy headers (x-forwarded-for, then x-real-ip), combines it with your server-side salt, and stores only:

hashing
ip_hash = sha256(ip + CANOPY_IP_SALT).slice(0, 16)

The salt (CANOPY_IP_SALT) never leaves your server, so the stored value is useless to anyone who obtains the database without the salt — a preimage attack would require guessing the entire salt. Keep the salt stable if you rely on returning-visitor counts: rotating it changes every hash from that point forward. And set a strong value in production; the built-in default exists only for local development.

5Do Not Track

The tracker checks the browser's Do Not Track signal (navigator.doNotTrack and its legacy variants) on every send and drops all events when it is enabled — before any network request is made. This is the default and needs no configuration. Overriding it is possible but not recommended:

not recommended
<script src="/p.js" data-site-id="canopy_example_com"
  data-respect-dnt="false" async></script>

6What this means for compliance

GDPR

Canopy collects no special-category data, stores nothing on the visitor's device, and pseudonymizes the IP before storage — the combination most privacy regulators treat as out of scope for consent-banner requirements for cookieless analytics. Because all data lives in your own Postgres database, you can fulfill access and deletion requests directly with SQL, and the export endpoint gives visitors their data on request.

Self-hosting

Self-hosting moves data residency entirely into your hands: choose a region, choose a provider, keep the database in the EU if that is what your audience requires. The dashboard is the only component with accounts, and analytics events are never tied to them — events know a site, a session, and a hash.

good practice
Mention the analytics in your privacy policy anyway ("we collect cookieless usage statistics"), scrub emails and tokens from event props with a beforeSend hook, and keep your CANOPY_IP_SALT secret. The events guide shows the hook.

Analytics without the baggage

No cookies, no consent banner, no third-party requests — by construction.