=== Snorklee Analytics – Cookieless GDPR Web Analytics ===
Contributors: moovswell
Tags: analytics, gdpr, cookieless, privacy, google analytics alternative
Requires at least: 5.0
Tested up to: 7.1
Requires PHP: 7.4
Stable tag: 3.0.0
License: MIT
License URI: https://opensource.org/licenses/MIT

See which AI crawlers read your site — GPTBot, ClaudeBot, PerplexityBot. Cookieless, EU-sovereign analytics. No consent banner.

== Description ==

Snorklee Analytics is the official WordPress plugin for Snorklee — a privacy-first, 100% EU-sovereign analytics service built for the AI era.

= See the readers JavaScript cannot see =

GPTBot, ClaudeBot, PerplexityBot, Amazonbot and their peers fetch your HTML without ever running JavaScript. To any client-side analytics tool — Snorklee's own snippet included — they are invisible. The plugin detects them **server-side**, on the User-Agent of the page request itself, and reports the visit. That is the one thing this plugin does that no snippet can do, and it is why it exists.

A hit claiming to be OpenAI, Anthropic or Perplexity from outside the address ranges they publish is left out — a useful check, not a proof of identity, and Snorklee never calls a robot "verified". In your dashboard you get, as each robot declares itself: which AI systems read you, how often, which pages they take, and how many visitors AI assistants send you (a minimum, since most AI apps hide where visitors come from). Snorklee's AI page also suggests a robots.txt block, to copy into your site, if you want to let some of them in and keep others out.

= Three ways to install, and what each one costs =

The plugin injects a single `<script>` tag on every public page. You choose how:

1. **Standard.** Script and measurements both come from snorklee.com. Full measurement — but blockers that filter on the domain cut everything, and depending on your audience **5 to 20% of sessions silently disappear**.
2. **Script served by WordPress (recommended).** WordPress serves the tracker from your own domain (`/js/flow.js`), while the measurements are sent by the visitor's browser. The script escapes blockers that filter on the filename, and everything is still measured: country, unique visitors, traffic sources.
3. **Full relay (self-host).** WordPress also passes the measurements through, so the visitor's browser makes 1st-party requests only and no blocker sees anything. **The cost is real and we state it plainly:** Snorklee then only ever sees your server, so there is no country and no region, and unique-visitor counts and traffic-source attribution stop being reliable — they are derived from that same address. Your dashboard shows an empty map and a repair task rather than a false region.

Why the relay cannot keep geolocation: the plugin does forward the visitor's address, but Snorklee refuses to turn a declared address into a location. On a public endpoint, anyone could claim any address they like. Trusting it would mean publishing a map built on hearsay.

No DNS configuration, no Nginx tweak, no cPanel access. **Works on every WordPress hosting**, including OVH/IONOS/Infomaniak shared plans.

= Privacy by construction =

* No cookie set on the visitor's browser
* No persistent identifier (visitor hash rotates daily, salt rotates monthly)
* No cross-site tracking (each site has an isolated identifier space)
* No advertising classification (no UTM parsing, no paid/organic split)
* Honors `Do Not Track`, `Sec-GPC`, and a localStorage opt-out flag
* Designed so that the ePrivacy consent requirement (article 5(3)) is **not triggered** : nothing is stored on or read from the visitor's device — documented in the Snorklee compliance dossier

= What the plugin does NOT do =

* No phone-home from PHP for visitor tracking in standard mode (everything happens client-side) — the only server-side call is the optional AI-crawler beacon described under "External services", disabled with one toggle
* No tracking pixel injected by the plugin itself
* No data collection beyond what the standard Snorklee JS tracker does
* No third-party scripts loaded
* No SQL table created (the plugin only stores 4 options : domain, dashboard URL, install mode, AI-crawler beacon toggle)
* No personal data stored on your WordPress server (in full-relay mode, events transit through but are not logged)

= Why use the plugin instead of pasting the snippet manually =

* Survives theme updates (the snippet sits in plugin code, not in `header.php`)
* Settings page with built-in installation test (HTTP probe on home page)
* Self-host mode in one click (impossible without writing PHP yourself)
* Multisite-compatible
* Clean uninstall : no leftover options, no SQL tables, no transient
* MIT-licensed source code, fully auditable

== External services ==

This plugin connects to the Snorklee analytics service (https://snorklee.com), which is what it exists for. Three kinds of requests are involved :

1. **Visitor tracking (standard mode)** : the visitor's browser loads the tracker from `snorklee.com/w.js` and sends pageview events to `snorklee.com/api/event`. Data sent : page path (query string stripped), referrer, viewport size, user agent. The visitor's IP is used transiently for country/region geolocation and is never stored. No cookies, no persistent identifier.
2. **Visitor tracking (script or full-relay mode)** : the same data. In "script served by WordPress" mode only the tracker file transits through your site; in full-relay mode the events do too, forwarded server-side to `snorklee.com`.
3. **AI-crawler beacon (server-side, optional, on by default)** : when a known AI crawler (GPTBot, ClaudeBot, PerplexityBot…) requests a page, the plugin reports that crawl server-to-server to `snorklee.com/api/event` (crawler user agent + page path, query string stripped). This concerns bot traffic only, never your human visitors. One toggle on the settings page disables it.

Service provider : Snorklee (France). Terms : https://snorklee.com/docs/en/gdpr · Privacy : https://snorklee.com/en/privacy-compliance · Data collected : https://snorklee.com/docs/en/data-collected

== Installation ==

= Method 1 : upload the ZIP (recommended) =

1. Download `snorklee.zip` from your Snorklee dashboard (Integration tab → WordPress plugin card).
2. In wp-admin → Plugins → Add New → Upload Plugin, select the ZIP.
3. Activate the plugin.
4. Go to Settings → Snorklee.
5. Enter your **site domain** (e.g. `yoursite.com`) — the plugin suggests it automatically based on `home_url()`.
6. Save. Tracking is active immediately on every public page.

= Method 2 : choose your install mode =

After step 6 above, on the same settings page, pick one of the three modes. Each one shows what it costs, so read them before choosing:

7. **Script served by WordPress** — recommended. Escapes blockers that filter on the filename, keeps the full measurement.
8. **Full relay** — escapes every blocker, but gives up country, region, reliable unique visitors and traffic-source attribution. Choose it only if blocked visits matter more to you than knowing where your readers come from.
9. Save, then click "Test installation" to confirm everything works.

= Verifying =

Visit any public page of your site. Check the Snorklee dashboard's "Verify installation" card. Within a few seconds, your pageview should appear. The integration tab also shows which mode is detected. For the AI-crawler side there is nothing to click: the next time GPTBot or ClaudeBot fetches a page, it shows up in the AI Traffic tab — that can take hours or days, depending on how often they come.

== Frequently Asked Questions ==

= Where is my site domain ? =

It's the domain under which your site is publicly accessible (e.g. `mysite.com`, without `www.` and without `https://`). The plugin auto-suggests it based on `home_url()` — usually a single click to fill it in.

= Does the plugin track admin pages ? =

No. The snippet is only injected on public pages (frontend). The wp-admin area is excluded.

= Do I need a cookie banner ? =

**No.** Snorklee sets no cookie and stores nothing on the visitor's device, so the ePrivacy consent requirement (article 5(3)) is not triggered. Compliance is documented in the Snorklee compliance dossier (Compliance tab of the dashboard).

= How does full-relay (self-host) mode work ? =

The plugin registers WordPress rewrite rules that map `/js/flow.js` and `/api/{event,ping}` to internal handlers. These handlers fetch the upstream resource from `snorklee.com`, cache the tracker JS for 1 hour (Transients API), pass events through, and return the response. From the visitor's browser perspective, all traffic is 1st-party — ad-blockers cannot tell it's analytics.

The plugin forwards the visitor's address in `X-Forwarded-For`, but Snorklee does not use it to place the visitor on the map, and you should not expect it to. `/api/event` is a public endpoint with no authentication: an address that merely arrives in a header is a claim, not an observation, and a map built on claims is worth nothing. So in this mode there is no country and no region, and unique visitors and traffic sources become unreliable — the visit key is derived from that same address, so distinct visitors merge. Pick "Script served by WordPress" instead if that matters more to you than the last few percent of blocked visits.

= Will full-relay mode slow down my site ? =

Marginally. The tracker JS itself is cached for 1 hour, so most visitors hit a near-instant cache. Each event proxy adds a few milliseconds of server time (an outbound HTTP request to snorklee.com), but the visitor's browser uses `sendBeacon()` which is non-blocking — invisible to the user even if the proxy adds 50 ms of latency.

= What happens if snorklee.com is down ? =

In full-relay mode, if the upstream is unreachable when serving the tracker JS, the plugin returns a no-op stub script (`/* tracker upstream unavailable */`) so your page does not break. Events sent during the outage are lost (no offline queue, by GDPR minimization doctrine). The plugin re-fetches the tracker on the next request once upstream recovers.

= Can I change mode later ? =

Yes, anytime. Pick another mode and save: the plugin flushes the rewrite rules and the next page view uses the new one. Nothing is migrated and nothing is lost — the mode only decides which route the measurements take.

= Does the plugin work with caching plugins (WP Rocket, W3 Total Cache, etc.) ? =

Yes. The tracker is enqueued through the standard WordPress script API (`wp_enqueue_script`) and printed in the `<head>`, so caching plugins capture it like any other script. The proxied tracker JS uses standard `Cache-Control: public, max-age=3600`, which CDNs and WP cache plugins handle correctly.

= How do I uninstall completely ? =

Deactivate and delete the plugin from wp-admin → Plugins. The plugin removes its 3 options + tracker cache automatically (multisite-safe). No SQL table to drop. No leftovers.

= Where does the tracker source code live ? =

The plugin proxies the official snorklee tracker, served from `snorklee.com/w.js` (or your custom dashboard URL). Source code of this plugin : in your `wp-content/plugins/snorklee/` folder, plain PHP, no obfuscation.

== Screenshots ==

1. Settings page : domain field + the three install modes, each stating its cost
2. Status card : which mode is active, and the exact snippet it injects
3. Installation test result : "Tracker correctly installed in self-host mode (1st-party detected)"

== Changelog ==

= 3.0.0 =
* **Three install modes instead of a toggle, and each states its cost.** The new "Script served by WordPress" mode serves the tracker from your domain while the measurements go straight from the visitor's browser: you escape blockers that filter on the filename, and you keep country, unique visitors and traffic-source attribution. It is now the recommended mode.
* **Correction of a measurement claim that was false.** Earlier versions said that forwarding `X-Forwarded-For` let snorklee.com see the visitor's real IP for geolocation. It never did: Snorklee has always refused to geolocate a declared address on a public endpoint. Sites using full-relay mode were therefore having all of their traffic placed at their own host's location. Measured on a live site on 2026-08-29: 100% of its visits, AI crawlers included, were located in the region of its hosting provider. The mode selector, the readme and the documentation now say what each mode really costs.
* **AI crawler identity: the address sent is the one your server actually observed.** The beacon used to read the FIRST entry of `X-Forwarded-For` — the declarative part of the chain, which the caller writes. A scraper sending a published OpenAI address alongside a "GPTBot" label got itself reported as verified. It now reads the last hop, the one added by your proxy, and sends nothing at all rather than a private or unusable address.
* Existing installs are picked up silently: self-host on becomes full relay, self-host off becomes standard. Nothing to redo.
* **Single-page themes and headless front-ends are now counted page by page.** The tracker served by the plugin follows page changes made through the History API (themes and front-ends built with React, Vue, Next.js or Nuxt): each change sends the previous page with its time and scroll depth. Only the path is sent, never the query string or fragment.
* The plugin description no longer promises features Snorklee has removed (robots.txt compliance verdicts, a queue of recommended actions): the AI page now shows only what is actually captured.

= 2.3.7 =
* AI crawler detection: the bot's IP address is now sent in a dedicated header. Behind a reverse proxy, `X-Forwarded-For` could be rewritten in transit, so Snorklee lost the crawler's address and could no longer confirm its identity against the ranges published by OpenAI, Perplexity or Anthropic — those hits showed up as "declared" instead of "verified". Update if your AI Traffic tab shows almost no verified bot.

= 2.3.6 =
* The behavioural cards (scroll depth, attention zones, UX friction) have been removed from Snorklee (they fell outside the strict audience-measurement scope). The self-host proxy no longer maps `/api/zone`; the rewrite rule is dropped automatically on update.

= 2.3.5 =
* The click-heatmap feature has been removed from Snorklee (it fell outside the strict audience-measurement scope). The self-host proxy no longer maps `/api/click`; the rewrite rule is dropped automatically on update.

= 2.3.4 =
* Install test now correctly reports self-host mode : it detects the 1st-party `/js/flow.js` route (WordPress rewrites the enqueued path to an absolute same-origin URL, which the previous scheme-based check misread as 3rd-party). Tracking itself was unaffected — only the test verdict.

= 2.3.3 =
* Plugin URI and Author URI are now distinct (Plugin URI points to the plugin docs page, Author URI to the company site)

= 2.3.2 =
* No inline `<script>` left : the admin-menu "open in new tab" behavior moved to an enqueued `assets/admin-menu.js` (data passed via `wp_localize_script`)
* The settings-page snippet preview is now built without a literal script-tag string in the source (it was display-only text, never injected)
* Removed an outdated source-code URL from the readme

= 2.3.1 =
* Plugin Check (PCP) pass for the wordpress.org submission : zero error, zero warning
* The tracker `<script>` tag is now enqueued via `wp_enqueue_script` + `script_loader_tag` (same rendered tag, standard API)
* All `$_SERVER` reads go through `wp_unslash()` + `sanitize_text_field()` / `esc_url_raw()`
* `wp_parse_url()` replaces `parse_url()` in the AI-crawler beacon
* Multisite uninstall uses `get_sites()` instead of a direct SQL query
* Removed a vestigial `load_plugin_textdomain()` call (the plugin ships its own translation files)

= 2.3.0 =
* Top-level "Snorklee" menu in the admin sidebar, with a direct "Open dashboard" link to your Snorklee dashboard (new tab)
* The plugins-list action link now reads "Settings" (localized), following the WordPress convention
* Settings page unchanged, now reachable via the Snorklee menu

= 2.2.0 =
* UI now available in 6 languages : French, English, German, Italian, Spanish, Dutch (was FR + EN)
* Locale variants fall back to their language file (de_AT → de_DE, es_MX → es_ES, en_GB → en_US…)
* Documentation links now point to the localized docs (6 languages)
* Wording aligned with the current Snorklee compliance framing (ePrivacy article 5(3) not triggered)

= 2.1.1 =
* Fix : uninstall now also removes the `snorklee_crawler_beacon` and `snorklee_rewrite_version` options (clean uninstall)
* Fix : AI-crawler beacon strips the query string from the reported path (consistent with the JS tracker, never stores campaign parameters)

= 2.1.0 =
* Rewrite rules now re-flush automatically on plugin update (no manual reactivation needed)

= 2.0.0 =
* Self-host mode (1st-party proxy) built into the plugin via WordPress REST + rewrite rules
* Installation test button (HTTP probe with i18n result messages)
* UI in 6 languages : French, English, German, Italian, Spanish, Dutch (extensible : drop a `languages/<locale>.php` file)
* Settings page redesign with the snorklee brand palette (dark mode)
* Fix : default dashboard URL was incorrect in v1.0.0 (`app.snorklee.com` → `snorklee.com`)
* Fix : Plugin URI / Author URI now point to the correct `.com` domain
* CNIL referential update : July 2025 (was 2020-091 in v1.0.0)
* Multisite uninstall improved (cleans up new option keys + transient cache)

= 1.0.0 =
* Initial release. Auto-inject snippet on `wp_head`, settings page, multisite-safe uninstall.

== Upgrade Notice ==

= 2.0.0 =
Major upgrade : self-host mode (anti ad-blocker, anti Chrome Tracking Protection) and several v1 bugs fixed (default URL was broken). Re-save your settings after upgrade — the option key changed from `wml_site_id` to `snorklee_site_domain` for clarity.
