=== NP Visitor Journeys ===
Contributors: norttipaikalle
Tags: analytics, privacy, statistics, referrers, utm
Requires at least: 5.8
Tested up to: 7.0
Requires PHP: 7.4
Stable tag: 2.3.3
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Privacy-friendly page view and referrer analytics for WordPress. No cookies, no third-party tracking, no fingerprinting.

== Description ==

NP Visitor Journeys is a lightweight, self-hosted analytics plugin that tracks page views, referrers, outbound link clicks, and UTM campaign parameters directly in your WordPress database — with no external services involved.

**Key features**

* Page view and referrer tracking, including a "Visitor Journeys" view showing the path visitors take through your site.
* Outbound click tracking.
* UTM campaign parameter tracking (source, medium, campaign, term, content).
* Optional daily CSV export/backup of collected data.
* No cookies, no localStorage, no device fingerprinting, and no data sent to third-party analytics services — all data stays on your own server.
* IP address pseudonymisation (hashing) enabled by default.

= Privacy and data collection =

This plugin is designed to minimise the personal data it collects, but it does still process some personal data (primarily IP addresses) in order to function. See "Privacy and GDPR" under Frequently Asked Questions below for full details on what is collected, how it is stored, and how it can be removed.

== Installation ==

1. Upload the plugin files to the `/wp-content/plugins/np-visitor-journeys` directory, or install the plugin through the WordPress plugins screen directly.
2. Activate the plugin through the "Plugins" screen in WordPress.
3. Visit the "NP Visitor Journeys" menu in your WordPress admin to view stats and configure settings.

== Frequently Asked Questions ==

= What data does this plugin collect? =

For each page view: the page URL, page title, referrer URL, referrer type, UTM campaign parameters (if present in the query string), a timestamp, and the visitor's IP address. For each outbound link click: the destination URL and domain, a timestamp, and the visitor's IP address.

No cookies are set, no browser storage (localStorage/sessionStorage) is used, and no device or browser fingerprinting is performed.

= Is the IP address pseudonymised? =

By default (setting can be changed under NP Visitor Journeys > Settings), IP addresses are hashed with SHA-256 combined with a site-specific secret before being stored, so the raw IP address is never written to the database. This is more accurately described as **pseudonymisation** rather than full anonymisation: the same visitor will consistently produce the same hash on a given site (this is used deliberately to power the "Visitor Journeys" grouping feature), so under GDPR this hashed value is still treated as personal data. If you disable this setting, raw IP addresses are stored instead.

= How long is data kept? =

By default, tracked hits and clicks older than 14 months are deleted automatically (checked weekly). You can change this retention period, or set it to 0 to keep data forever, under NP Visitor Journeys > Settings > "Data Retention". You can also clear all data immediately at any time with "Clear All Stats" on the same page.

= What happens to my data if I delete the plugin? =

By default, all collected data is kept in the database if you delete the plugin, so you don't lose your historical stats by mistake. You can change this under NP Visitor Journeys > Settings ("Data on uninstall") to have the plugin remove its options only, or delete all collected data and options, when it is uninstalled.

= Can I erase data for a specific visitor if they request it? =

Yes. NP Visitor Journeys > Settings > "Erase Visitor by IP Address" lets an administrator delete all tracked hits and clicks for a given IP address — it matches both raw and pseudonymised (hashed) storage, so it works regardless of the current IP anonymisation setting. This plugin does not use WordPress core's built-in Tools > Export/Erase Personal Data screens, because those are keyed by email address and this plugin does not collect one — IP address is the only visitor identifier it stores.

= Does this plugin send data to any third party? =

No. The plugin makes no outbound network requests to any third-party service, and does not load any tracking scripts on your site's front end.

= Privacy and GDPR =

This plugin processes IP addresses (pseudonymised by default) and referrer/UTM data as part of its normal operation, which under GDPR constitutes processing of personal data. Site owners are responsible for ensuring their own privacy policy discloses this processing and for identifying an appropriate legal basis (commonly "legitimate interest" for first-party web analytics that does not use cookies or cross-site tracking). This plugin does not itself display a cookie-consent banner or gate tracking behind consent, since it does not use cookies.

== Screenshots ==

1. Main dashboard showing page views and top referrers.
2. Visitor journeys view.
3. Settings page.

== Changelog ==

= 2.3.3 =
* Fixed a real data-loss bug: importing a JSON backup exported from a completely different site replaced this site's data with no domain-mismatch warning at all. Root cause: a file with no (or an unparseable) site_url was treated as "nothing to compare, assume it's fine" rather than requiring confirmation — so any file missing that field bypassed the safety check entirely, regardless of its actual origin. The check now fails closed: only a file whose site_url is a POSITIVELY VERIFIED match for this site skips confirmation; anything else (a different domain, or no domain info at all) requires the "Import anyway" step.
* CSV (.zip) exports/imports never carried a site_url at all, so they had no domain-mismatch protection whatsoever, even before the above bug. CSV exports now include a small manifest.json with the site's URL, giving CSV imports the same protection JSON imports have always had.
* Reordered the Transfer data tab: Export, then Import, then (when applicable) the mismatch/unverified-origin confirmation card, so it appears directly below the action that triggered it. The two destructive confirm buttons ("Import and replace data", "Import anyway") are now styled red.

= 2.3.2 =
* Replaced 2.3.1's no-signal "in progress" bar with a real one: import now uploads in two steps — the file uploads via AJAX to a new fast validate-and-stash step (accurate upload percentage shown, since this request never touches the database and returns quickly), then the actual replace of hits/clicks (and settings, for a JSON file) is triggered as a plain page navigation, avoiding the exact host-timeout failure mode that broke 2.3.0's large imports. Domain-mismatch confirmation is unaffected — it's the same confirmation step as before, just reachable from either import path.

= 2.3.1 =
* Fixed a regression from 2.3.0: the Import form's XHR-based progress bar caused large imports (confirmed with a 12.7MB file, 35,000+ rows) to fail with a network error partway through server-side processing, on a real production install (norttipaikalle.fi), even though the same import worked fine on 2.2.x. Likely cause: a long-running XHR/AJAX request is subject to a shorter idle-timeout on some hosts' reverse proxy/CDN than a full-page navigation gets. The import form now submits as a normal page navigation again (the exact mechanism 2.2.x used), with the progress bar showing an honest "in progress, don't navigate away" indicator instead of a byte-accurate upload percentage.

= 2.3.0 =
* Added a CSV export/import option to Transfer data, alongside the existing full JSON backup. CSV export downloads hits.csv + clicks.csv zipped together — data only, no settings; useful for opening in a spreadsheet, or a data-only restore that leaves this site's own settings untouched. Import now accepts either a .json or .zip file and detects which by extension.
* Added a progress bar to the Import card: an "Uploading…" phase driven by real upload progress (useful for large multi-MB backups), then an animated "Processing…" phase while the server validates and writes the data, before automatically landing on the result notice.
* Fixed fputcsv()/fgetcsv() deprecation notices on PHP 8.4+ by passing the escape character explicitly.

= 2.2.5 =
* Fixed the 2.2.4 fix being incomplete: get_option()'s default argument only covers a setting that was never saved, not one whose row exists but literally holds `false` (found in the wild on norttipaikalle.fi, likely a carry-over from the old wpstats_* option migration). Export and import now both treat a stored `false` the same as "unset" for every affected setting (retention_months, delete_on_uninstall, ignored_referrers, tracked_selectors), so this class of bad-schema false-rejection can't recur regardless of what's actually sitting in wp_options.

= 2.2.4 =
* Fixed Transfer data export/import wrongly rejecting valid files with "File does not match the expected NP Visitor Journeys export format." Three settings (Ignored Referrers, CSV export, Track clicks on) are only saved to the database the first time their form is submitted; on a site where one had never been touched, get_option() returned false instead of a string, which got exported literally as `false` in the JSON and then failed import's type check. Export now supplies the correct default for every setting, and import now also tolerates `false` in these fields from files exported before this fix.

= 2.2.3 =
* Full-project sweep: Transfer data now includes the "Track clicks on" setting (previously silently dropped on export/import); guarded ajax_track_click() against a non-string url parameter that could fatal on PHP 8; removed the dead "Export all as .csv" handler superseded by Transfer data; added a length cap to the "Track clicks on" field.

= 2.2.2 =
* Reordered tabs to Dashboard, Documentation, Settings, Transfer data, License — Documentation now comes right after Dashboard (it explains every other tab) and License moved to the end, matching NP 301's convention of putting account/billing last.

= 2.2.1 =
* Documentation tab examples changed from this site's own real selectors (.kfc-buy, #livetods-ribbon) to generic placeholders (.buy-button, #signup-button), since this plugin is used across sites with different markup.

= 2.2.0 =
* Added a Documentation tab covering every tab's basic usage, plus a step-by-step guide to finding and adding a CSS selector under Settings > "Track clicks on" — including how to recognize when a button lives inside a third-party widget's Shadow DOM (in which case its own class/id can never be matched, and the widget's outer container must be used instead).

= 2.1.4 =
* Fixed tracked clicks disappearing from Visitor Journeys after any further page view was recorded for the same visitor. A click only ever counted as an "exit click" if it was the visitor's chronologically last action in the period — any later browsing (by them, or coincidentally the same IP) pushed it out of view, even though the click still happened. A visitor's most recent click in the period now always shows, regardless of what they did afterward.

= 2.1.3 =
* Fixed clicks silently not appearing in Visitor Journeys on sites using full-page caching (LiteSpeed Cache, WP Rocket, etc.): a cached page load skips WordPress/PHP entirely, so the page-view "hit" for that visit never gets recorded, even though the click itself (an always-executes AJAX call) still does. Visitor Journeys previously required a matching hit to exist before it would show a visitor's click at all, silently dropping click-only visitors. A click now always gets its own journey entry, with or without a page-view hit alongside it.

= 2.1.2 =
* Replaced the "Tracked Events" tab (2.1.0/2.1.1) with a much simpler mechanism: a "Track clicks on" field on the Settings tab, listing CSS selectors (one per line) to record as a click even when there's no real cross-domain link. Matching clicks now show up automatically as the same exit-click pill already used in Visitor Journeys — labeled with the matched selector, the same way a shared link shows its domain — with no separate tab, no click log to browse, and no new database columns.
* Fixes a real regression from 2.1.0/2.1.1: the clicks table gained two new columns (source_url, tracked_event_id) that ordinary outbound-click tracking was made to depend on. On a site where that schema change didn't fully apply, every click — including plain outbound links that were working before — silently failed to record. This release removes that dependency entirely; outbound-click tracking is back to exactly its pre-2.1.0 behaviour.

= 2.1.1 =
* Tracked Events "Add" form now defaults to CSS selector (was Domain / URL pattern) — logging a click regardless of destination is the more common need, and the field's placeholder/help text now updates to match whichever type is selected instead of showing both examples at once.

= 2.1.0 =
* Added a "Tracked Events" tab: define a domain/URL pattern or a CSS selector to follow, and see the full visitor journey (pages visited, in the 24 hours before) leading up to each matching click — useful for JS-driven buttons (e.g. an embedded ticket-shop widget) that never produce a real cross-domain navigation. Domain/URL events match retroactively against clicks already recorded; CSS selector events are matched live in the visitor's browser going forward.
* The clicks table now also stores which page a click happened on (source_url), used by Tracked Events' journey view.

= 2.0.0 =
* Renamed throughout to NP Visitor Journeys: plugin folder/slug (np-stats → np-visitor-journeys), main file, all classes/functions/hooks/options, the two database tables, and CSS/JS classes. Existing installs are migrated automatically on update — the database tables are renamed in place (no data loss) and saved settings are copied across; no action needed.
* Added a License tab (LicMan integration). Top Referrers and UTM Campaigns are now premium features — unlicensed sites see a locked upsell card instead of the live data; everything else (Dashboard totals, Top Pages, Visitor Journeys, Settings, Transfer data) remains free.
* Breaking: the plugin's WordPress folder name changed from np-stats to np-visitor-journeys — on the next update, delete the old np-stats plugin folder after installing this version (WordPress treats it as a new plugin, not an in-place update, since the folder name changed).

= 1.5.3 =
* Transfer data import now checks the uploaded file's recorded site_url against this site's domain (not the filename). If they differ, it warns and requires an explicit "Import anyway" confirmation before replacing any data.

= 1.5.2 =
* Transfer data export filename now includes the site's domain (e.g. np-visitor-journeys-transfer-example.com-2026-08-10.json), so exports from different sites are easy to tell apart.

= 1.5.1 =
* Fixed the Settings and Transfer data tabs' cards sitting flush against the tab row with no top spacing.

= 1.5.0 =
* Added a "Transfer data" tab: export all tracked hits, clicks, and settings to a single JSON file, and import a previously exported file to restore or move that data to another site. Import replaces existing data, and requires an explicit confirmation before running.

= 1.4.4 =
* WordPress.org Plugin Check follow-up: wrapped uninstall.php's logic in a function so its local variables no longer register as unprefixed globals, and corrected three phpcs:ignore comments whose line placement didn't actually cover the flagged code (the underlying code was already safe — table names built from wpdb->prefix, never user input — this only fixes the suppression, not the logic).

= 1.4.3 =
* Documented (via a justified phpcs:ignore) the shared `nortti_paikalle_parent_page()` function's naming-convention warning — it's deliberately identical across every Nörtti paikalle plugin so they share one top-level admin menu; a per-plugin prefix would break that.

= 1.4.2 =
* Removed a filter on the update-transient that suppressed a stale entry for this plugin's old folder name — WordPress.org Plugin Check flags any code that touches this transient as a "plugin updater" (not permitted for hosted plugins), and the folder-collision issue it worked around no longer applies once distributed through the directory under an approved slug.

= 1.4.1 =
* WordPress.org Plugin Check pass: corrected text domain to match the plugin slug (np-visitor-journeys) and removed the unused Domain Path header, escaped remaining unescaped output, switched date() to gmdate() for timezone safety, added wp_unslash() before sanitizing request data, and annotated database queries that build table names internally (never from user input).
* Bumped "Tested up to" to 7.0.

= 1.4.0 =
* WordPress.org & GDPR compliance milestone: added readme.txt and declared a GPLv2-or-later license.
* Bundled Chart.js locally instead of loading it from a third-party CDN, so wp-admin no longer makes an external request on every dashboard load.
* Added automatic data retention: tracked hits/clicks older than a configurable period (default 14 months) are now pruned weekly. Configurable under Settings > Data Retention; set to 0 to keep data forever.
* Added suggested privacy-policy text on Settings > Privacy describing this plugin's data collection.
* Added an "Erase Visitor by IP Address" tool on the Settings tab to fulfil individual data-erasure requests (matches both raw and pseudonymised/hashed storage).
* Corrected "Anonymise IP Addresses" wording to "Pseudonymise IP Addresses" throughout the settings UI and documentation — the hashing this option applies is pseudonymisation, not full anonymisation, and GDPR treats pseudonymised data as still personal data.

= 1.3.3 =
* Renamed display name to "NP Visitor Journeys".

= 1.3.2 =
* Added pagination to Top Pages, Top Referrers, and UTM Campaigns.

= 1.3.1 =
* Maintenance release.

== Upgrade Notice ==

= 1.4.0 =
WordPress.org & GDPR compliance milestone: license declaration, locally-bundled Chart.js, automatic 14-month data retention, privacy-policy content suggestion, per-IP data erasure, and corrected pseudonymisation wording. No breaking changes.
