# Full Changelog Archive

This file preserves the complete version history. readme.txt's Changelog section is kept
trimmed to recent versions only, since WordPress.org's readme parser truncates that section
at 5000 characters (a limit this plugin's history exceeded — see the 2026-07-05 Plugin Check
report: readme_parser_warnings_trimmed_section_changelog).

= 1.20.0 =
* Added: the Landing Page Optimization Report has been substantially redesigned — canonical Source and Medium normalization across grouping, filters, and CSV; a landing-page-centered grouping model (Source/Medium/Campaign as additive breakdowns, never a replacement); full column sorting; KPI cards and comparison-period trend badges shared with the Executive Dashboard; new Top Insights, Top Rankings, and Paid vs Organic sections surfacing a small number of prioritized, minimum-sample-gated findings instead of raw data; and a simple Report Notes field for recording what changed during the analyzed period. The Leads Table also gains a Medium filter alongside the existing Source filter, with matching CSV export support.
* Fixed: landing page URLs are now normalized before aggregation (tracking parameters like utm_source/fbclid/gclid no longer split one physical page across multiple report rows), with automatic backfill for existing data; the Leads Table Form filter now groups by the visible form name instead of the internal Form ID, so the same form embedded across different pages or widget instances no longer appears as duplicate dropdown entries — CSV export uses the identical rule. Several Plugin Check findings (input sanitization and SQL-escaping hardening on the Landing Page report and Leads Table) have also been resolved. The Leads Table Source filter dropdown no longer folds organic social traffic (e.g. Facebook/Instagram referrals with a "social" medium) into the "Meta (paid)" option, which could make that traffic unselectable even though it was visible in the table; the dropdown is now also scoped to whatever other filters (Medium, date range, Form, search) are currently active, so it only ever offers values that actually exist under the current view.
* Improved: rate metrics across the Landing Page report show "Insufficient data" instead of a misleading 0% on a zero-session denominator; a new "No Tracked Outcome Rate" replaces ambiguous abandonment framing the session model cannot actually support; reporting helpers (KPI cards, sortable columns, comparison-period math) are now shared between WC Analytics and the Landing Page report instead of duplicated.

= 1.19.0 =
* Added: the Reporting API settings section and its generated credentials are now described as a vendor-neutral integration point for any authorized external reporting, analytics, or BI system, rather than implying one specific platform. The three Historical Attribution Repair tools (Sessions, Leads, Orders) now show a live eligible-record count, an always-visible button, and a detailed scanned/repaired/already-valid/failed summary after each run.
* Fixed: the Executive Dashboard's Cancellation Rate no longer double-counts cancelled orders in its denominator. Source/medium normalization (meta_ads, google-shopping, and existing Meta aliases) is now applied consistently across the Dashboard, Campaign/Product Performance, Orders table, CSV exports, and the Reporting REST API. The Leads Table's single-lead Edit modal now reliably saves — a nonce collision with the bulk "Edit UTM" action, and a leads-table cache that wasn't invalidated on write, could previously make a correction silently fail to appear.
* Improved: Product Performance no longer shows a misleading zero for per-product Checkout counts, which aren't measurable at that granularity; a zero-volume Conversion Rate now shows "Insufficient data" instead of 0%; manually correcting a lead's Source now also updates its last-touch source, so Top Source reporting reflects the correction.

= 1.18.0 =
* Added: admins can now correct human errors directly in the Leads Table — edit a lead's name, email, phone, custom fields, and UTM values via a compact modal, or bulk-correct UTM values across multiple selected leads at once. Corrections write directly to the canonical record (no override layer) and reuse the same normalization the Attribution Engine already applies to newly-captured data.
* Fixed: several Attribution Engine consistency gaps — WooCommerce's native order CSV export now carries the same canonical fields as the plugin's own export; a landing-session fallback signal that was being silently dropped is now used; WooCommerce Analytics event write failures are now logged instead of disappearing silently; report filters now normalize free-text source/medium input to match stored data; six additional paid-platform click IDs (TikTok, LinkedIn, Bing, etc.) are now recognized for attribution inference; the attribution repair tool is now available for Leads and WooCommerce Orders, not just Landing Page Sessions.
* Hardening: malformed cookie values are now safely rejected instead of risking corrupted data; Elementor's custom-URL-parameter hidden fields now get the same plausibility validation as resolver-fed fields.

= 1.17.2 =
* Fixed: `WordPress.DB.DirectDatabaseQuery.DirectQuery`, `.NoCaching`, and `PluginCheck.Security.DirectDB.UnescapedDBParameter` kept reappearing on already-annotated direct-query lines across the last three releases, despite correctly-formatted `phpcs:ignore`/`phpcs:disable`/`phpcs:enable` comments. Root cause, confirmed via an isolated, empirical PHPCS+WPCS reproduction: every class file opens with `phpcs:disable WordPress.Files.FileName.InvalidClassFileName` and never closes it. PHPCS keeps ignore-state as one running variable per file; because it was never null again, every later properly-paired `phpcs:disable/enable` block for unrelated codes elsewhere in the same file ended up registering those codes into an internal exception list that then silently cancelled every subsequent same-code `phpcs:ignore` comment. Fixed by closing each file-level disable immediately (one line later), across all four affected files. No functional change — purely a lint-suppression correctness fix, verified with zero remaining warnings.

= 1.17.1 =
* Fixed: a critical error on the Landing Page Optimization Report when a session row's landing_page was empty/null (a legitimate, nullable column) — the new path/summary/copy-URL display code didn't account for that. Hardened at multiple points (source data, the display helper, and the view) plus a defensive fallback around the new per-row rendering, so a data edge case here can degrade gracefully instead of taking down the whole report.
* Fixed: two Plugin Check warnings on the new attribution-repair queries (missing phpcs:ignore for interpolated table name in a multi-line query) — same underlying lint-suppression pattern issue fixed in 1.16.2, now applied correctly with a disable/enable block since these particular queries span multiple lines.
* Hardened: get_report_rows() no longer risks a fatal if a query ever fails at the database level (defensive-only; not something that occurred here as far as could be confirmed).

= 1.17.0 =
* Fixed: a session-attribution bug where many Landing Page Optimization Report rows showed "untracked" (or a stale/wrong source) even though the landing page URL clearly had utm_source/utm_medium/gclid/fbclid. Root cause: the session-start beacon fired before the browser's UTM (and, on a brand-new visitor, first-touch) cookie had been updated for that pageview, so the server read stale or empty cookie data instead of this visit's own. Fixed at the source (JS ordering + a direct landing-page-URL fallback, reusing the existing Attribution Engine — no new resolution rules).
* Added: a one-time, safe, idempotent repair tool (Settings → Landing Page Optimization Report) that rebuilds affected historical rows from their own already-stored landing page URL. Only fills in fields that are currently blank/"untracked"; never overwrites anything else.
* Added: Landing Page Optimization Report columns are now resizable by dragging the column border, with a "Reset column widths" button — reusing the exact same mechanism as the Leads table, each table's widths stored separately.
* Improved: the Landing Page column now shows a compact path + short attribution summary (based on the current grouping) instead of the full URL; the full URL is still available via tooltip and a one-click "Copy full URL" button.
* Fixed: the "Landing Page Optimization Report" settings section was sandwiched in the middle of "First Click + Last Click Forms," splitting it visually and logically. It's now its own section, appearing after Forms is complete.

= 1.16.2 =
* Fixed: Plugin Check flagged three direct-query lines in the Landing Page Sessions table (retention cleanup and both session upserts) as unescaped/uncached/unprepared, despite each already carrying a `phpcs:ignore` comment listing exactly those codes. The comment was split across two lines — a standalone one above the query, plus a separate one-code comment at the end of the query line itself — and the second overrides the first rather than merging with it. No functional change: these queries were already safe (table/column names are hardcoded, values go through `$wpdb->prepare()`); this only consolidates each pair into a single same-line directive so the suppression actually takes effect.

= 1.16.1 =
* Fixed: the WooCommerce Orders — Attribution Campaign filter was collected and echoed back into the filter input, but never actually applied to the query — selecting a campaign had no effect on the table, the order count, or CSV export. It's now applied consistently across all three, alongside the existing Status/Source/Search filters.

= 1.16.0 =
* Added: Landing Page Optimization Report grouping — switch between Landing Page only, Landing Page + Traffic Source (previous default behaviour), and Landing Page + Campaign, to see whether performance issues trace to the page, the traffic source, or the campaign. New "Session data retention" default is now 365 days (was 180), for year-over-year and seasonal-campaign comparisons out of the box; 90/180/Forever remain available.
* Fixed: WooCommerce Orders — Attribution date filter always displayed "Today" regardless of the actual selection, even though filtering itself worked correctly.
* Fixed: several CSV exports (Landing Page Optimization, Cart Events, Campaign Performance, WooCommerce Orders) could silently ignore filters that were visibly active on screen, since these export forms submit via POST while the underlying filter-reading code only checked GET parameters. Exports now always match what's currently shown in the table/report.

= 1.15.0 =
* Added: Landing Page Optimization Report — an aggregated (not per-visitor) report answering "I sent traffic to this landing page, what happened next, and did my changes improve performance?" Shows Sessions, Leads, Lead Rate, Add To Cart, ATC Rate, Cart Abandonment, Purchases, Purchase Rate and Revenue per landing page + first-touch source, with date/UTM/landing-page/conversion filters, column visibility, CSV export, and an optional period-over-period comparison. WooCommerce-only columns auto-hide when WooCommerce tracking is off.
* Added: a new Landing Page Sessions table — one row per visitor session (never per pageview, never a click-path), created via a single beacon at session start and updated only on a meaningful outcome (lead / add_to_cart / begin_checkout / purchase). Exists solely to power the report above; does not change or re-run the Attribution Engine — only stores its already-resolved values.

= 1.14.1 =
* Fixed: bulk-deleting selected leads did nothing. The table's bulk-action form submits via GET (matching how WordPress's own list tables work, so search/sort/pagination stay plain bookmarkable links), but the delete handler only ever checked $_POST — so the selected checkboxes were silently never seen. Also added the nonce check that was missing entirely before (previously only a warning-suppression comment, not an actual check).
* Added: a date range filter on the Leads table, alongside the existing Form/Source filters — Today, Yesterday, Last 7 days, Last 30 days, This week, Last week, This month, Last month, Maximum (no filter, default), and a Custom range with two date pickers. Respected by the table view, pagination/sorting, and CSV export alike.

= 1.14.0 =
* Attribution Engine Hardening, based on a full third-party engine review. Unified referrer classification into one canonical map that both PHP and JS now share (previously maintained separately and confirmed to have drifted — e.g. WhatsApp/Telegram classified as "messaging" in JS but "organic" in PHP); fixed substring-based referrer host matching (e.g. "notgoogle.com" could previously false-match "google.com") to exact/subdomain matching. Added canonical source/medium normalization (lowercase/trim/synonym mapping) to prevent reporting fragmentation from case/spacing variants. Added server-side capture of UTM parameters directly from the current request and the referrer's own query string, so a conversion no longer loses campaign attribution if JavaScript, cookies, or storage are blocked or unavailable. Added integrity verification for the first-touch cookie (HMAC-style, using WordPress's own site secrets) since it's designed to be write-once. Added plausibility validation for hidden-field attribution values. Fixed several medium/low findings: stale session_count and inaccurate landing_page on fully-direct sessions, checkout-dedupe now using the same 30-minute session model as the rest of the plugin, legacy order-meta fallback gaps in WC Analytics, CSV filename date-timezone consistency, and widened the leads table's session_count column. No changes to the Canonical Attribution Dataset's field list or contract.

= 1.13.3 =
* Plugin Check remediation: readme.txt's Changelog section exceeded WordPress.org's 5000-character limit (was ~37,500 characters) and was being silently truncated by the readme parser. Trimmed to recent versions only; full history preserved in this file. No code changed.

= 1.13.2 =
* Bug fix: Column Visibility save was broken for every column (built-in and custom alike). Root cause: the per-custom-field inline delete control introduced in 1.13.1 was implemented as a <form> nested inside the "Visible Columns" <form>. Nested forms are invalid HTML — browsers close the OUTER form as soon as they parse the first inner form's closing tag, which happened before the "Save Column Visibility" button later in the markup. That left the save button outside any form element, so clicking it submitted nothing; the checkboxes you saw and the columns actually saved were completely disconnected. This only manifested once at least one custom field existed (which is why it could look "general" — the whole form broke, not just the custom field's own control, hence affecting every column, built-in or not).
* Fixed by replacing the nested form with a plain button bound to a separate, sibling <form> via the standard HTML5 form="" attribute — no nesting, same inline delete UX.
* No changes to the underlying visibility-resolution logic (resolve_leads_hidden_columns(), the hidden-columns diff calculation, or CSV export column selection) — all of that was already correct; the bug was purely in the HTML structure preventing the form submission from ever reaching the server correctly.

= 1.13.1 =
* Custom Lead Fields UX refactor: management moved from Settings to the Attribution Dashboard (Leads) page, next to Visible Columns, via a new "+ Add Custom Field" button that opens a modal. No change to Attribution Dataset, resolution logic, or UTM logic.
* Added: editing existing custom fields (label, Show in Table, Include in CSV) directly in the modal. Field ID itself is immutable after creation, since it is the same key already used inside existing leads' stored custom_fields JSON — allowing it to change would silently orphan historical values under the old key.
* Added: a quick delete (✕) directly next to each custom field's checkbox in Visible Columns, in addition to delete from within the modal. Deleting only removes the field's configuration/registration — historical values already stored in existing leads' custom_fields JSON are never touched or deleted.
* Storage architecture unchanged: still one custom_fields JSON column, no per-field database columns. No schema change in this release.
* Table and CSV behavior unchanged from v1.13.0 (custom fields already displayed as clean per-field values, never raw JSON, and already CSV-gated independently via Include in CSV) — this release only relocates and completes the management UI (add/view/edit/delete) that was previously add/remove-only and lived on the wrong page.

= 1.13.0 =
* UX improvement: fresh installs (and any user who has never saved column-visibility preferences) now see a curated default set of columns in the Leads table — Conversion Time, Form, Name, Email, Phone, UTM Source/Medium/Campaign/Content, First Touch Source, Last Touch Source, Conversion Page, and Confidence — instead of every attribution column at once. Display only: every Canonical Attribution Dataset field is still fully persisted, still available in column settings, and still available in CSV export regardless of this default. Existing users' saved column preferences — including a preference to show every column — are always respected exactly and never overridden.
* New: Custom Elementor Lead Fields. Site owners can configure additional Elementor Field IDs (e.g. "city", "budget") to be captured, displayed, and exported without writing code, via Settings → Custom Lead Fields. This is conversion data, not attribution data — it has no effect on source resolution, normalization, or classification, and is entirely separate from the Canonical Attribution Dataset.
* Custom field values are stored as a single JSON blob in one new "custom_fields" column, not as one database column per field, so the schema stays stable regardless of how many custom fields a site configures.
* Custom fields reuse the existing shulmanattrib_extra_lead_columns developer filter as their registration mechanism, so they automatically participate in the same column-visibility toggle and generic column rendering as any other column, with no new UI plumbing.
* Custom fields have two independent settings: "Show in table" (seeds the default-visible state, same role as the built-in default list above) and "Include in CSV" (an independent gate applied only at export time — a field can be hidden from the table but still exported, or shown in the table but excluded from export).
* Database: adds one column (custom_fields, longtext) to the leads table. Existing leads are unaffected and simply have no custom field data until the feature is configured and new leads are submitted.
* No changes to attribution resolution logic, UTM normalization, first/last touch logic, or WooCommerce attribution logic. docs/ATTRIBUTION_DATASET.md required no changes.

= 1.12.2 =
* Plugin Check remediation: added explicit, justified suppression annotations (PluginCheck.Security.DirectDB.UnescapedDBParameter) to the two $wpdb calls in the v1.12.1 Leads search/filter query builder. The underlying query was already safe — $sql is assembled entirely from hardcoded string fragments, and every user-supplied value is bound through $wpdb->prepare()'s own %s placeholders — but Plugin Check's static analyzer cannot trace that dataflow through a dynamically-concatenated query string, so it flagged the pattern defensively. No query logic changed.
* No other code changes. A Plugin Check run against this version also reported ~350 "Text Domain Mismatch" findings; these are a false positive caused by the plugin folder being scanned under a renamed directory (e.g. "shulman-utm-attribution-for-elementor1.12.1") rather than its correct slug — the plugin's actual Text Domain header and every translation call correctly use "shulman-utm-attribution-for-elementor" throughout. No action taken, since "fixing" this would mean incorrectly embedding a version number into the text domain, which would break translations and violate WordPress.org header requirements. Re-run Plugin Check against a correctly-named plugin folder to confirm a clean result.

= 1.12.1 =
* Closes the last identified Canonical Attribution Dataset parity gap: Leads search (both the admin Leads table's live search and the Leads CSV export's search/filter) now includes first_touch_medium, first_touch_campaign, first_touch_content, first_touch_term, and session_count, alongside the fields that already participated in search.
* No attribution logic changed, no database schema changed, no new features — this release only closes the Persisted → Read → Searchable gap for fields that were already being persisted.
* Refactored the Leads CSV export's search/filter query: replaced four hand-written, near-identical SQL strings (each with its own manually-counted list of %s placeholders) with a single dynamic WHERE-clause builder, matching the pattern already used by the admin Leads table's own search. This was the reason the fields above were never added previously — extending the old structure safely would have meant re-counting placeholders by hand across four separate strings. The new builder generates one placeholder and one bound value together in the same loop for every search column, so they cannot drift out of sync, and any future field can be added by adding one array entry.
* Search/filter behavior is otherwise unchanged: the same combinations of keyword search, form filter, and source filter produce the same results as before, now simply checking five additional columns for the keyword search.

= 1.12.0 =
* Attribution hardening phase — brings the plugin into full compliance with docs/ATTRIBUTION_DATASET.md. No changes to attribution decision logic (source resolution, normalization, direct/untracked classification) — this release is exclusively about closing Read/Used/Displayed/Exported gaps for fields the engine already resolves, and eliminating duplicated logic.
* WooCommerce Orders: single-order admin meta box now shows the full canonical dataset (first touch, last touch, click IDs, confidence, session count, journey fields) in a collapsed "Full attribution detail" panel — shown only when data is present, so the default view doesn't grow denser.
* WooCommerce Orders CSV export (native Orders screen): extended with the full canonical dataset (First/Last Touch, gclid, fbclid, Confidence, Session Count, Landing/Conversion Page, Referrer), appended after the existing columns so no existing column position shifts for anyone parsing the export by index.
* Elementor Leads: registered first_touch_medium/campaign/content/term as selectable admin-table columns, which — via the existing generic column mechanism — also makes them available in the Leads CSV export with no export-specific code required.
* CSV hardening: all four CSV exports in the plugin (Leads, Orders, and all four WooCommerce Analytics exports) now run every value through one shared sanitizer that strips tags, decodes entities, and neutralises formula-injection payloads (values beginning with =, +, -, @). The Leads export and three of the four WooCommerce Analytics exports previously wrote raw, unsanitized values.
* Shared Attribution Logic Audit: removed a duplicated CSV-sanitization implementation from the WooCommerce Analytics module in favour of the single shared implementation on the core engine. Pre-purchase WooCommerce Analytics events (add to cart, begin checkout) now run through the same classify_missing_attribution() direct/untracked classification pass that Elementor Leads and WooCommerce Orders already used, closing the one remaining path that resolved direct/untracked using a narrower internal check.
* Session Count Parity: added a non-blocking click listener for WooCommerce Blocks Checkout's place-order button (in addition to the existing native-form-submit listener), since Blocks Checkout submits via the Store API and does not fire a native form submit event. This only reads localStorage and rewrites the tracking cookie — it never calls preventDefault or interferes with the checkout click in any way.
* All schema/behavior changes in this release are additive; no existing leads, orders, order meta keys, cookies, or CSV column positions were altered or removed.

= 1.11.0 =
* Canonical Attribution Dataset (CAD v1.0): Elementor Leads, WooCommerce Orders, and WooCommerce Analytics Events now persist a consistent, agreed field set instead of three independently-evolved schemas. No new attribution logic was added — this release closes gaps where fields already resolved by the shared attribution engine (resolve_attribution_dataset() and related methods in shulman-utm-attribution-for-elementor.php) were being computed and then discarded before being written to a given conversion type's storage.
* Elementor Leads (wp_ute_leads table, DB schema 1.6.0): now stores the full first-touch UTM set — first_touch_medium, first_touch_campaign, first_touch_content, first_touch_term — alongside the existing first_touch_source. Previously only first_touch_source was persisted.
* WooCommerce Orders: save_utm_to_order() now writes last_touch_source, gclid, fbclid, referrer, landing_page, conversion_page, and session_count to order meta. These were already being resolved in-memory for classification purposes but were not previously persisted.
* WooCommerce Analytics Events (DB schema 1.1.0): new columns first_touch_content, first_touch_term, last_touch_source, gclid, fbclid. Purchase events now read the click IDs and last-touch source from the order meta written above; pre-purchase events (add to cart, begin checkout) read them directly from the shared attribution engine's resolved dataset.
* Architecture: session_count is now read via a single shared get_session_count() method on the core engine (with a get_session_count_public() accessor), removing a duplicated cookie-read implementation that previously existed independently in both the Elementor lead-save path and the WooCommerce Analytics module.
* No changes to attribution decision logic itself (source resolution, normalization, direct/untracked classification) — this release is exclusively about persisting fields the engine already computes.

= 1.10.25 =
* Follow-up hardening to the WooCommerce Orders CSV export (native Orders-screen "Export CSV" button, stream_orders_csv() in shulman-utm-attribution-for-elementor.php): the refund guard no longer relies solely on `instanceof WC_Order`. It now also rejects any order object whose class name contains "refund" (catching WC_Order_Refund, Automattic\WooCommerce\Admin\Overrides\OrderRefund, or any future variant, regardless of what it does or doesn't extend), and every individual getter call (get_billing_first_name(), get_billing_last_name(), get_billing_email(), get_billing_phone(), get_status(), get_total(), get_payment_method_title()) is now routed through a method_exists() check before it is ever invoked. This makes a "Call to undefined method" fatal structurally impossible in this loop, independent of any particular class hierarchy assumption. The same layered guard was applied to the WooCommerce Analytics dashboard's own orders list (get_wc_orders_with_attribution() in class-wc-analytics.php).
* Note: this plugin update was authored and reviewed via static code inspection; it was not run against a live WooCommerce store with refund records during development. If a CSV export failure persists after installing this version, please confirm the file actually deployed matches this version (check the version string in Settings, and clear any object cache / OPcache) and report back with the order ID it stops at so the exact record can be inspected.

= 1.10.24 =
* Safety: uninstall.php no longer deletes any plugin data by default. Deleting the plugin from Plugins → Delete now preserves the wp_ute_leads table, the WooCommerce Analytics events table, all plugin settings, the WooCommerce integration setting, and per-user leads-table column preferences — a reinstall picks up right where you left off.
* New setting: "Delete all plugin data on uninstall" (Settings → Danger Zone), off by default. Only when this box is explicitly checked and saved will uninstall.php drop the plugin's tables and options on deletion. Clearly marked as destructive/irreversible, with a confirmation prompt before saving.
* Fix: the "Export CSV" button on the native WooCommerce → Orders screen (admin_post_shulmanattrib_export_orders / stream_orders_csv()) could fatal-error and write a WordPress HTML error page into the downloaded .csv file when a refund record (WC_Order_Refund) was present in the result set, because WC_Order_Refund shares the WC_Abstract_Order base class with WC_Order but does not implement get_billing_first_name() and related accessors. The export now excludes refunds at the query level (wc_get_orders( ['type' => 'shop_order'] )), adds a runtime instanceof WC_Order guard as a second line of defense, and wraps each row in its own try/catch so a single unexpected record is skipped instead of aborting the whole export or leaking an error page into the file. The export always produces a valid CSV now, even when refunds exist in the store.
* Hardening: the same refund-safe query filter, instanceof WC_Order guard, and per-row try/catch were also applied to the WooCommerce Analytics dashboard's own Orders list/export (get_wc_orders_with_attribution() / export_wc_orders_csv()) as defense in depth.
* Hardening: all WooCommerce Orders CSV export values (both the native Orders-screen export and the WooCommerce Analytics dashboard export) are now passed through a clean-value helper that strips HTML tags and decodes HTML entities before being written to the CSV, guaranteeing the export is always plain text data rather than rendered markup.
* No changes to normal plugin update behavior, activation/deactivation, or existing UTM tracking/attribution logic.

= 1.10.23 =
* Updated plugin description and feature list in readme.txt.
* Fix: all number_format_i18n() output across WC Analytics views and class-wc-analytics.php is now wrapped in esc_html() to satisfy WordPress output-escaping requirements. No display change — numbers are formatted identically.
* Fix: added translators comments above translation strings containing placeholders (Orders count, Events count) where missing.
* Fix: $_GET['paged'] in page_wc_orders() and page_wc_cart_events() is now read via wp_unslash() and absint() before use, matching the sanitization pattern already used elsewhere in the plugin. No behavior change — paged was already cast to (int) before use.
* Hardening: all WC Analytics view-file local variables now use the shulmanattrib_ prefix (e.g. $row → $shulmanattrib_row, $export_nonce → $shulmanattrib_export_nonce) to satisfy WordPress.NamingConventions.PrefixAllGlobals. No behavior change — purely a renaming pass.
* Hardening: WC Analytics SQL queries that interpolate the trusted table name ({$table}) and pre-built WHERE/IN fragments ({$where_sql}, {$id_placeholders}) now carry explicit phpcs:ignore annotations with rationale, consistent with the existing pattern already used in admin/class-leads-table.php. These values are never derived from user input — $table comes from $wpdb->prefix + a hardcoded literal, $where_sql and $id_placeholders are built exclusively from hardcoded SQL fragments and %s/%d placeholders, with all actual values passed through $wpdb->prepare(). No query logic changed.
* This is a code-quality and WordPress Plugin Check hardening pass only — no tracking, attribution, or reporting behavior was changed.
* Follow-up fix: replaced several single-line phpcs:ignore comments on multi-line SQL statements in wc-analytics/class-wc-analytics.php and admin/class-leads-table.php with phpcs:disable / phpcs:enable blocks spanning the full statement. A phpcs:ignore comment only suppresses the single physical line it is attached to; on a $wpdb->prepare() call whose SQL string spans several lines, this left interior lines (e.g. the line containing {$table} or {$where_sql}) still flagged. No query text or logic changed — this corrects the placement of suppression comments only, and adds the WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare sniff code to the ignore lists where it was previously missing.

= 1.10.22 =
* Feature: WooCommerce Analytics module — a completely separate dashboard from the Elementor Leads dashboard.
* New admin submenu structure: Settings / Attribution Dashboard / WooCommerce Dashboard / WC Orders / Cart Events / Products / Funnel / Campaign Performance.
* New database table: {prefix}shulmanattrib_wc_events — dedicated to WooCommerce events, never touches wp_ute_leads.
* New class: ShulmanAttrib_WC_Analytics — singleton, bootstrapped inside init_woocommerce_integration() only when WooCommerce is active and enabled.
* Attribution engine: zero duplication — all WC Analytics uses the existing resolve_attribution_dataset(), merge_attribution_datasets(), read_utm_cookie(), read_fc_cookie(), server_side_fallback(), normalize_paid_meta_source(), get_order_utm_meta(), get_order_fc_meta() from the core class via thin public accessor wrappers.
* Event tracking: Add To Cart, Remove From Cart (server-side via WooCommerce hooks); Begin Checkout (AJAX from wc-analytics.js on checkout page, once per session); Purchase (server-side, reads attribution from order meta written by existing save_utm_to_order() — no duplicate attribution writes).
* Purchase events mark matching Add-to-Cart and Begin-Checkout events for the same visitor as purchased=1, enabling funnel conversion analysis.
* Executive Dashboard: 14 KPI cards (Revenue, Orders, ATC, Begin Checkout, Purchases, Conversion Rate, ATC Rate, Cart Abandonment, Checkout Abandonment, AOV, Top Campaign, Top Source, Top Product, Top Landing Page) + day-by-day revenue bar chart (Chart.js).
* Date range filter on all dashboards: Today, Yesterday, Last 7 Days, Last 30 Days, This Month, Custom Range.
* WC Orders screen: enriched order list with attribution from order meta (HPOS + legacy compatible), source/status/campaign filters, search, pagination, CSV export.
* Cart Events screen: paginated event log with event type, product, session count, source, purchased flag, source filter, date filter, CSV export.
* Products screen: per-product aggregates (ATC, Remove, Checkout, Purchases, Revenue, ATC→Purchase %, Revenue per ATC, Top Source, Top Campaign), sortable, CSV export.
* Funnel screen: 4-stage visual funnel (Visitors → ATC → Checkout → Purchase) with conversion and abandonment rates; filterable by source, medium, campaign, product.
* Campaign Performance screen: GROUP BY source/medium/campaign/content/term aggregates (Visitors, ATC, Checkout, Orders, Revenue, Conv %, AOV, Rev/Visitor, Cart Abandonment), filterable, CSV export.
* All report queries use wp_cache_get/set (2-minute TTL) to avoid N+1 and repeated DB hits on large stores.
* Visitor identity: persistent shulmanattrib_wc_vid cookie (2-year lifetime). Session ID derived deterministically from visitor ID and 30-minute window — matches utm-tracker.js SESSION_GAP.
* Device, OS, browser detection from User-Agent for event storage.
* Frontend JS (wc-analytics.js): lightweight, depends on utm-tracker.js, sends Begin Checkout AJAX once per page session (sessionStorage guard), uses fetch with XHR fallback.
* Security: AJAX uses check_ajax_referer; admin exports use check_admin_referer + manage_woocommerce capability check; all DB values prepared or sanitized before storage.
* Backward compatibility: existing Elementor Leads, Attribution Dashboard, WooCommerce attribution, cookies, localStorage, order meta, and DB tables are fully preserved and unmodified.
* Developer hooks: shulmanattrib_wc_event_types filter (add custom event types); shulmanattrib_wc_before_store_event filter (modify event data before insert).
* DB version: shulmanattrib_wc_db_version = 1.0.0 (separate from shulmanattrib_leads_db_version).

= 1.10.21 =
* Fix: First Click localStorage now expires after FC_COOKIE_DAYS (400 days) instead of EXPIRY_MS (7 days). First Click storage is correctly write-once and cannot be overwritten by later visits.
* Fix: After applying filters, searching, sorting, or paginating the leads table, the browser scrolls back to the leads table (#ute-leads-table anchor) instead of the top of the page.
* Fix: Clearing the search field and submitting now removes the search parameter from the URL and returns the table to unfiltered results (empty search no longer re-submits the previous search term).
* Feature: Column resize improved — uses getBoundingClientRect() for accurate start-width, RTL-safe delta direction, cursor indicator during drag, e.stopPropagation() to prevent accidental sort clicks.
* Feature: "Reset column widths" button added above the leads table. Clears all saved column widths from localStorage and restores defaults immediately. Does not affect lead data.
* Feature (Phase 2): Session count tracking. A new shulmanattrib_session_count localStorage key counts real sessions (30-minute inactivity gap = new session). session_count is included in the attribution cookie snapshot and stored in a new session_count column (TINYINT UNSIGNED DEFAULT 1) in wp_ute_leads. The leads table shows a "Sessions" column (sortable). CSV export includes session_count. Existing leads without session_count display 1 as a safe fallback.
* Feature (Phase 3): Developer filter shulmanattrib_extra_lead_columns allows registering additional Leads Table columns by Elementor field ID, without modifying plugin code. Values are pulled from saved lead DB rows; missing values show empty. Column visibility, CSV export, and sorting of extra columns are all supported. Built-in column IDs are protected from override.
* DB migration: schema version bumped to 1.5.0. dbDelta() safely adds the session_count column to existing installations without data loss.

= 1.10.20 =
* Removed the remaining Plugin Check warnings in the leads table by inlining the final count/items SQL execution path instead of passing intermediate query-string variables to $wpdb methods.
* Kept the safe prepared-query flow, source filter compatibility, sorting, pagination, and caching introduced in 1.10.19.

= 1.10.19 =
* Refactored the leads table query builder to use a single prepared-query flow for search, source filters, form filters, sorting, and pagination.
* Fixed the malformed SQL / placeholder mismatches that triggered Plugin Check prepared SQL errors and unescaped DB parameter warnings in class-leads-table.php.
* Added object-cache wrapping for lead count and lead list queries to address direct-query caching guidance in the admin table.

= 1.10.18 =
* Fix: extracted direct/untracked classification into a private classify_missing_attribution() method. Both build_lead_snapshot() (Elementor lead path) and save_utm_to_order() (WooCommerce path) now call this shared method, ensuring consistent behaviour. The WooCommerce built-in attribution signal (wc_source) is included as a signal on the WooCommerce path and correctly absent on the Elementor lead path.
* Fix: attribution confidence resolved by resolve_attribution_dataset() is now preserved through build_lead_snapshot() when meaningful attribution exists. Confidence is only overridden in the direct/untracked branch.
* Fix: extracted a private get_known_referrer_map() method. Both resolve_source_from_referrer() and server_side_fallback() use this single map, eliminating the divergence between the two functions. Added missing platforms from resolve_source_from_referrer(): youtu.be, t.me, telegram.me, reddit, pinterest, snapchat, duckduckgo, baidu, yandex.
* Fix: server_side_fallback() now matches referrers against the parsed host only (not strpos() on the full URL). This prevents false positives where a domain like notgoogle.com could have matched the 'google.' pattern.
* Fix: removed the incorrect $ute_direct_count variable from leads-page.php (it was computed as total minus tracked, which double-counts both direct and untracked together). All percentage calculations and template references already used the correct $ute_direct_only_count and $ute_untracked_count from dedicated DB queries.
* Fix: lead deletion in leads-page.php now performs wp_safe_redirect() + exit after a successful delete. The ?action=delete&lead_id= URL is no longer left in the browser's address bar. The success notice is displayed on the next page load via a short-lived user transient.

= 1.10.17 =
* Fix: replaced single-line phpcs:ignore comments with phpcs:disable / phpcs:enable blocks around the two multiline $wpdb->get_var() / $wpdb->prepare() queries that span multiple lines. The previous inline ignore was placed on the SQL string line but PHPCS was flagging the get_var() and prepare() lines above it — the disable/enable block now covers the entire multiline statement correctly.

= 1.10.16 =
* Fix: added phpcs:ignore annotations to all dashboard query lines that reference $source_filter_sql. PHPCS static analysis cannot trace the variable origin and incorrectly flagged it as an unescaped or unprepared DB parameter. The value is derived entirely from hardcoded strings inside build_source_filter_clause() — no user input reaches the SQL fragment directly.
* Fix: updated "Tested up to" header in readme.txt from 6.9 to 7.0.

= 1.10.15 =
* Feature: resizable columns on the Attribution Dashboard leads table. Column widths are persisted in localStorage and survive page refresh. A resize handle appears on each column header; drag left/right to resize. Minimum column width: 60 px. Horizontal scroll is preserved. Sorting, filtering, bulk actions, pagination, CSV export, and column visibility are all unaffected. localStorage access is wrapped in try/catch so the table remains fully usable when localStorage is unavailable (Private Browsing, restricted environments).
* Improvement: Form column in the leads table now displays plain text instead of a clickable filter link. Filtering by form is still available through the Form dropdown above the table.
* Feature: paid Meta source normalisation. When utm_medium indicates paid traffic (paid, cpc, paid_social, ppc, display), source aliases fb / facebook / ig / instagram are normalised to the canonical value "meta" before storage. Organic Facebook and Instagram traffic is intentionally NOT normalised — platform-level attribution is preserved for organic sources. Implemented as a single reusable helper normalize_paid_meta_source($source, $medium) called from the attribution processing layer, ensuring consistent output across DB storage, Elementor hidden fields, WooCommerce order meta, webhook payloads, and CSV export.
* Improvement: normalisation applied in both required places — at the end of resolve_attribution_dataset() covering utm_source, last_touch_source, and first_touch_source; and in build_lead_snapshot() when setting first_touch_source from the FC snapshot, using the first-click medium as the paid-intent signal.
* Improvement: Attribution Dashboard source filter is backward-compatible with pre-normalisation records. Filtering by "meta" returns both new canonical "meta" records and legacy paid records stored as fb / facebook / ig / instagram. Legacy paid aliases are merged under a single "Meta (paid)" entry in the source dropdown.
* Fix: PHPCS/WPCS — replaced unreliable inline phpcs:ignore comments with a phpcs:disable / phpcs:enable block in enqueue_admin_assets() around the reads of $_GET['page'] and $_GET['post_type']. Added an explanatory comment confirming these are WordPress admin routing parameters, not user-submitted form data, and that nonce verification is not applicable.

= 1.10.14 =
* Fix: corrected inline PHPCS suppression placement for WordPress.Security.NonceVerification.Recommended warnings.
* Improvement: moved `phpcs:ignore` comments directly to the relevant `$_GET` access lines so static analysis correctly recognises the intentional nonce verification flow.
* No functional plugin behavior changes.

= 1.10.13 =
* Fix: CSV export now correctly sends a downloadable file instead of printing raw CSV text into the admin page.
* Fix: Export logic moved from the admin page callback (leads-page.php) to an `admin_init` hook in the main plugin class. This ensures HTTP response headers are set before WordPress outputs any HTML, which is the only reliable way to trigger a file download in the WordPress admin.
* Fix: Added `ob_end_clean()` loop before sending CSV headers to flush any output already buffered by WordPress or other plugins.
* Fix: UTF-8 BOM is now written via `fprintf()` instead of `echo`, avoiding encoding issues.
* Fix: `echo "ï»¿"` (a double-encoded BOM) replaced with the correct `"\xEF\xBB\xBF"` BOM byte sequence.
* Improvement: leads-page.php now contains an early-return guard for export requests, preventing any HTML from leaking into the response if the admin_init handler is somehow bypassed.
* Fix: admin-export-orders.js was an empty IIFE stub — the WooCommerce orders Export CSV button was unresponsive on click. Added a delegated click handler that builds and submits a POST form to admin-post.php with the correct action and nonce values.

= 1.10.12 =
* Fix: prevent stale "untracked" Elementor hidden-field values from conflicting with the final server-side attribution snapshot.
* Fix: ensure webhook / CRM / Google Sheets attribution data stays consistent with the Leads Dashboard resolver output.
* Improvement: hidden attribution fields may now replace weaker "untracked" values with resolved attribution data such as direct, google, facebook, whatsapp, etc.
* Improvement: "untracked" is now preserved only for real tracking failures or fully missing attribution signals.
* Added: should_overwrite_hidden_attribution_field() helper — centralises the overwrite decision for attribution field enrichment.

= 1.10.11 =
* Feature: automatic best-effort enrichment of Elementor hidden attribution fields
  before all form actions run (Webhook, Email, CRM, Elementor Submissions, etc.).
* Enrichment is strictly limited to fields whose Elementor type is "hidden".
  Visible form fields are never touched under any condition.
* Two enrichment paths:
  - Known attribution field IDs (utm_source, utm_medium, utm_campaign, utm_content,
    utm_term, gclid, fbclid, referrer, landing_page, conversion_page,
    first_touch_source, last_touch_source, confidence, lc_source, lc_medium,
    lc_campaign, fc_source, attribution_source, attribution_medium) are filled
    from the central resolver (UTM → Click ID → Referrer → UA → First Touch →
    Direct → Untracked).
  - Custom hidden fields whose ID exactly matches a URL query parameter
    (e.g. utm_platform, camp_name, ad_id, adset_name) are filled from that
    parameter's raw value. No inference or mapping is applied to custom fields.
* Generic aliases (source, medium, campaign) removed from resolver-based enrichment
  to prevent conflicts with unrelated form fields.
* Existing field values are never overwritten — only empty recognised fields are enriched.
* Strictly non-blocking and fail-safe: if enrichment fails for any reason, form
  submission continues normally. No fatal errors, no stopped submissions, no changes
  to required field validation or user-facing fields.
* WP_DEBUG mode: logs a short, data-free diagnostic message if enrichment throws.

= 1.10.10 =
* Fix: prevent direct traffic from being incorrectly classified as untracked.
* Fix: ensure last_touch_source stays consistent with resolved utm_source.
  Previously, when utm_source resolved to "direct", last_touch_source was incorrectly
  set to "untracked" because is_meaningful_attribution_source() excludes "direct".
  last_touch_source now correctly mirrors "direct" when that is the resolved state.
* Fix: improve webhook / Elementor hidden-field data consistency for unattributed traffic.
* Improvement: clearer separation between direct and untracked states across resolver,
  lead snapshot, and WooCommerce order meta.

= 1.10.9 =
* Fixed: confidence is now explicit when both utm_source AND utm_medium are present in the URL,
  even when fbclid is also present. fbclid no longer downgrades confidence from explicit to inferred
  when full explicit UTM exists. Partial UTM + fbclid correctly stays inferred.
* Added: com.google.android.googlequicksearchbox referrer is now classified as google / organic.
  This covers traffic from Android Google app surfaces (Google Discover, Search widget, Lens, Google app).
  Previously this could fall through to an untracked or referral classification.
  
= 1.10.8 =
  Explicit UTM → Click IDs → Referrer → User-Agent → First Touch → Direct → Untracked.
* Improved: fbclid split logic — fbclid alone now resolves to facebook/social (organic intent).
  Only upgrades to facebook/paid when utm_campaign, utm_content, utm_term, or dynamic ad variables are present.
* Fixed: utm_medium=social (and any explicit utm_medium) is never overwritten by click ID inference.
* Improved: direct vs untracked are now clearly separated. "direct" means tracking is functioning
  but no attributable source was found. "untracked" is reserved for complete tracking failure
  (all signals absent: no UTM, no click ID, no referrer, no UA match, no cookie, no first-touch).
* Added: attribution confidence field (explicit / inferred / fallback / untracked) stored
  in DB and order meta.
* Fixed: server_side_fallback no longer returns "untracked" for missing/internal referrers;
  the caller now decides direct vs untracked based on full signal inventory.
* Improved: WooCommerce orders column and meta box now distinguish direct vs untracked visually.
* Improved: Dashboard UTM source filter now lists direct and untracked as separate options.
* Updated: DB schema version 1.4.0 — adds confidence column to wp_ute_leads table.
* Updated: utm-tracker.js — fbclid click ID split logic, confidence field in cookie snapshot,
  internal navigation returns "direct" not "untracked".

= 1.10.7 =
* Fixed: fbclid normalization no longer overrides an explicitly set utm_medium=social.
  Previously, any visit with fbclid and utm_medium=social (e.g. an organic Facebook post
  tagged manually) would have its medium silently replaced with paid_social.
  Now only empty or "none" medium values are inferred as paid_social from fbclid presence.

= 1.10.6 =
* Fixed remaining Plugin Check SQL issues (prepared statements compliance)
* Improved security of database queries.
* Cleaned uninstall logic for safer execution.

= 1.10.5 =
* Removed remaining prebuilt SQL variables and inlined prepare() calls.
* Reworked admin queries to avoid interpolated SQL variable patterns flagged by review.
* Kept phone capture, dashboard display, and CSV export support intact.

= 1.10.4 =
* Fixed admin SQL queries to use prepared statements consistently.
* Escaped interpolated table names in dashboard queries.
* Added missing translators comments and refreshed readme metadata.

= 1.10.3 =
* Added plugin readme file for compliance.
* Improved static analysis compatibility for admin SQL and translations.
* Kept lead phone field support in DB, admin table, and CSV export.

= 1.10.2 =
* Added lead phone field support.
* Added phone column to leads screen and CSV export.
