# Changelog

All notable changes to BW Lead Attribution Intelligence are documented here.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.0.1] - 2026-04-13

### Changed
- **Default Click-ID Inference now includes `gbraid` and `wbraid`** on the
  `google/cpc` row. These are Google Ads' privacy-safe click identifiers used
  on iOS 14.5+ where Apple's App Tracking Transparency blocks the standard
  `gclid` mechanism. `gbraid` covers app-to-web clicks (ad tapped inside an
  iOS app) and `wbraid` covers web-to-app clicks. Without these in the
  Click-ID list, iOS-origin Google Ads traffic was getting misattributed as
  direct or falling through to referrer classification.
- Existing installs keep their current click-IDs — defaults only apply to
  fresh installs. Update the `google/cpc` row in Settings → Click-ID
  Inference manually if you're already running 1.0.0 and want the new params.

## [1.0.0] - 2026-04-13

First stable release. Graduates the plugin from pre-1.0 pilot status now that the
unified mapping format, channel resolution, custom dimensions, and full summary
output have all settled down and tested end-to-end on a live Gravity Forms site.

### Fixed
- **Source/medium separator no longer loses its spaces on save.** The default
  separator is `" / "` (with spaces), but WordPress's `sanitize_text_field()`
  trims whitespace, so the first time a user hit Save Changes the separator
  silently became `"/"` and `{bw:source_medium}` rendered as `google/cpc`
  instead of `google / cpc`. The separator now uses a dedicated sanitizer that
  strips tags and control characters but preserves leading and trailing
  whitespace. Users can keep `" / "`, `" • "`, `" — "`, or whatever they like.
- **Invalid Parameter Aliases rows are now stripped on save, not just hidden on
  render.** Previously, saving a row with a reserved merge-tag label (e.g.
  `summary : foo, bar`) would emit a validation error but still write the row
  to the database — the parser dropped it on render, so the UI stayed clean,
  but the stored option carried a zombie row forever. Validation now
  surfaces the error AND strips the offending row from the saved string, so
  the stored state matches what the UI shows.
- **Test tab custom-dimension display label.** The URL simulator result table
  showed custom dimensions as `custom.match_type` — a leftover from before the
  0.8.0 format change when tags were `{bw:custom.key}`. It now shows the
  unprefixed `{bw:match_type}` label so the Test tab matches what the Help tab
  lists and what you actually type into hidden form fields.

### Notes
- No schema or settings migrations required. Sites already running 0.8.x will
  keep their existing settings as-is; the separator fix only applies the next
  time the settings form is saved. Users with a mangled `"/"` separator can
  open Settings, retype `" / "` into the Formatting & Debug section, and save.

## [0.8.2] - 2026-04-13

### Fixed
- **Channel rule labels containing `{bw:source}` / `{bw:medium}` now parse correctly.**
  The labeled-list parser split lines at the first `:` it saw, which ate the colon
  inside the `{bw:source}` substitution token and broke the `{bw:source} : */referral`
  default rule. Any channel rule whose label contained a brace-wrapped token
  silently failed to match, falling back to the raw `source / medium` string.
  The parser now tracks `{...}` brace depth and splits at the first `:` that sits
  outside any group, so labels like `{bw:source}` round-trip cleanly. This bug was
  present in 0.7.0 but only surfaced now because referrer-medium visits rarely hit
  the default rule order during earlier testing.

## [0.8.1] - 2026-04-13

### Changed
- **Summary format polish.** The attribution and journey blocks now surface custom
  dimensions alongside the standard extras, so any dimension you've defined in
  Parameter Aliases flows into `{bw:summary}` / `{bw:summary_detailed}` automatically.
  - Source / medium line is lowercase (`source: google | medium: cpc`).
  - Extras line uses the real tag keys (`campaign: test | match_type: test match type`)
    instead of the previous human-labeled format (`Campaign: X | Keyword: Y`). This
    lets custom dimensions slot in naturally.
  - New `Channels: A, B, C` line under the stats block, listing distinct channels
    walked during the journey in most-recent-first order.
  - Stats label tweak: `Visits - 4` instead of `Visits: 4`.
  - Journey `Tag Info` line now shows source/medium with parentheses stripped
    (`source: direct`), omits `(none)` medium, and still collapses to
    `Tag Info - none` when a visit has nothing meaningful to report.
- **Parameter Aliases UI: Source and Medium promoted to dedicated inputs.** These
  two dimensions are required for attribution to work, so they no longer live in the
  shared textarea where they can be accidentally deleted or renamed. The "Other
  dimensions & custom parameters" textarea below handles `campaign`, `term`,
  `content`, `adgroup`, and custom dimensions. Internally everything still flows
  into the same `parameter_aliases` setting — the split is UI-only. Clearing the
  source or medium input falls back to the defaults on save.

## [0.8.0] - 2026-04-13

### Changed
- **Unified mapping format.** All mapping-style settings (Parameter Aliases, Referrer
  Classification, Click-ID Inference, Channel Mappings) now share one format:
  `label : value1, value2, ...`. One textarea per section, one rule per line, walked
  top-to-bottom. Easier to copy-paste between sites and easier to extend.
- **Parameter Aliases merged with Custom Dimensions.** The old Custom Dimensions section
  is gone — custom dimensions are now just rows in Parameter Aliases with a non-standard
  label (anything other than `source`, `medium`, `campaign`, `term`, `content`, `adgroup`).
- **Custom-dimension merge tags are now unprefixed.** `{bw:match_type}` replaces
  `{bw:custom.match_type}`. Validation on save rejects custom-dimension keys that collide
  with reserved merge tag names (e.g. `source_medium`, `channel`, `summary`).
- **Click-ID Inference flipped orientation.** Was `gclid|google|cpc` (one param per line).
  Now `google/cpc : gclid, gclsrc` (label is the resulting source/medium, values are the
  params). Consistent with every other section — label on the left is always the result.
- **"Referrer Classification" renamed to "Default Referrer Classification"** with a note
  that it is only a fallback — explicit UTMs and click-IDs always override it. The label
  on each row is the medium the visit will be assigned (e.g. `organic`, `social`, `ai`),
  making it trivial to add new categories without code changes.

### Added
- Help tab "All available merge tags" table now lists every custom dimension you've
  defined, showing its merge tag and which URL parameters populate it. No more generic
  `{bw:custom.key}` placeholder.
- Custom dimensions are now registered in the Gravity Forms merge-tag dropdown, so they
  appear alongside the built-ins when building forms.

### Removed
- Settings keys `source_parameters`, `medium_parameters`, `campaign_parameters`,
  `term_parameters`, `content_parameters`, `adgroup_parameters`, `organic_sources`,
  `social_sources`, `custom_dimensions`. Replaced by `parameter_aliases`,
  `referrer_classification`, and the updated `click_ids` format.

### Upgrade notes
- No migration: old settings keys are silently dropped on first save and defaults are
  used for any new keys that aren't present. Any custom aliases, referrer sources, or
  click-IDs must be re-entered in the new format. Acceptable because 0.7.x has not
  shipped outside the test site.

## [0.7.0] - 2026-04-13

### Added
- **Channel mappings.** New Settings section that maps raw `source / medium` pairs to
  friendly channel labels (e.g. `google / cpc` → "Google Ads", `* / referral` →
  "`{bw:source}`"). Rules are ordered, first match wins, wildcards (`*`) are supported
  on either side, and labels can substitute `{bw:source}` / `{bw:medium}` with the
  actual visit values. Ships with sensible defaults covering the common ad networks,
  organic search, email, social, direct, referral, and unknown buckets.
- New merge tag `{bw:channel}` — friendly channel label for the last-touch visit, with
  fallback to raw source / medium if no rule matches.
- New merge tag `{bw:first_channel}` — friendly channel label for the first-touch visit.

### Changed
- Summary attribution block uses the channel label on the `Converted via - ...` and
  `Originally found via - ...` header lines. Raw source/medium still appears on the
  second line (`Source: google | Medium: cpc`) for machine-readable consumption.
- The Source/Medium and Campaign/Keyword/Content lines now use `:` separators
  throughout (`Source: google | Medium: cpc`) instead of the mixed ` - ` / `:` format —
  these are data lines, not header labels.
- Detailed and default journey visit headers now show the channel label
  (`2026-04-13 08:13 - Google Ads`) instead of raw source / medium.

## [0.6.1] - 2026-04-13

### Added
- **Submission tracking.** The capture script now records every form submission against
  the visit it happened in. Visits that converted are annotated with "Submitted on: <url>"
  in the summary, and sessions that converted more than once list each conversion on its
  own visit.
- New merge tag `{bw:summary_detailed}` — same attribution block as `{bw:summary}` but
  the journey expands each touchpoint into the full page list captured inside that
  visit, with submitted pages flagged inline as `*submitted*`.
- Detailed journey now includes a `Tag Info - ...` line per touchpoint showing the raw
  source/medium/campaign/keyword/content attribution (or `Tag Info - none` for untagged
  visits).
- Summary `Days to Conversion:` stat, computed from the first-visit timestamp.

### Changed
- `{bw:summary}` attribution block uses ` - ` label separators
  (`Converted via - google / cpc`), adds an explicit `Source - X | Medium: Y` line to
  each touchpoint, renames `Tagged:` to `Tagged Visits:`, and drops the `Sources:` line
  (redundant with the per-visit journey rows).
- Default `{bw:summary}` journey is now a single line per touchpoint
  (`DATE - source / medium -> landing page`). Numeric index prefixes (`1.`, `2.`) and
  `--` separators were removed.
- Detailed journey drops the `Pages:` header and per-page indentation — pages list flush
  against the visit header, with submissions flagged inline as `... *submitted*`.
- `Campaign / Keyword / Content` line now uses the leading-label format
  (`Campaign - spring-sale | Keyword: shoes | Content: test-sd`).
- Field renamed `Submitted from` → `Submitted on` to match the per-visit annotation.
- View storage cap raised from 10 to 50 entries so the detailed journey can cover longer
  sessions.
- Visits and views now carry a millisecond `ts` so views can be grouped under the visit
  they belong to.

## [0.6.0] - 2026-04-12

### Added
- New merge tag `{bw:last_page}` — the most recent landing page URL (complements `{bw:first_page}`).
- New merge tag `{bw:submit_page}` — resolves live to the current page URL at form submission
  time, so you always know which page the lead converted on.
- **Summary redesign.** `{bw:summary}` now produces a structured, human-readable report
  optimized for CRM textarea fields: conversion source at top, first-touch comparison when
  different, stats line, and a numbered journey with only non-empty dimensions shown.
- **Test tab: inline URL simulator.** Type any URL and click "Simulate" to see how the plugin
  would classify it — without navigating or storing anything. Biggest UX improvement.
- Test tab: resolved values and summary counters now display as clean key/value tables instead
  of raw JSON.
- Test tab: "Clear all tracking storage" now asks for confirmation before wiping data.
- Test tab: visit history and raw storage are now in collapsible `<details>` sections.
- Settings tab: sections (Parameters, Referrers, Click-IDs, Custom Dims, Formatting) are now
  collapsible for better first-time comprehension.
- Form Fields tab: prominent info notice for Gravity Forms users to use merge tags instead.
- Help tab: "Which merge tag should I use?" decision tree with common CRM scenarios.
- Help tab: full merge tag reference table with descriptions.
- Help tab: "How attribution works" section explaining first-touch / last-touch / full-journey.
- UTM Builder: "Open tracked URL" button on each item for quick testing.
- `BWLeadAI.simulateUrl(url)` public API method for programmatic URL resolution.

### Changed
- Capture script moved from `<head>` to footer — no longer blocks page rendering.
- Summary counter updates batched into a single localStorage read-write cycle (was 2-4 cycles).
- MutationObserver debounced via `requestAnimationFrame` to avoid redundant work on rapid DOM changes.
- Visit/view storage keys include a monotonic counter to prevent collisions within the same ms.

### Fixed
- **Critical:** Mid-session re-tagging silently dropped. Tagged visits (UTMs/click IDs) with
  a different source or campaign now always update `last` and record a new visit entry, even
  within the same browser session. This ensures last-touch attribution is correct for
  retargeting, email campaigns, and multi-step funnels. `original` (first touch) is preserved.
- Referrer hostname matching used substring (`indexOf`), causing false positives (e.g.
  "mybloggoogles.com" matched "google"). Now uses exact hostname or dot-boundary suffix match.
- `getParams()` split URL values on every `=`, truncating values like `utm_campaign=a=b=c` to
  just `a`. Now uses `indexOf('=')` + `slice` to preserve the full value.
- Default click-ID table mapped `fbclid` to `cpc`, but Facebook appends `fbclid` to all
  outbound clicks (organic posts, shares, etc.), not just paid ads. Changed default to
  `facebook / social`. Paid Facebook ads should have explicit `utm_medium=cpc` which wins
  via the resolution cascade.
- MutationObserver for dynamically injected forms used a stale state snapshot from page load.
  Now calls `buildState()` on each mutation for fresh data.

### Removed
- Dead `URL_LC` variable in capture.js (defined but never used).

## [0.5.0] - 2026-04-11

### Added
- Phase 1 rebuild from the legacy `bw-user-analytics` plugin.
- Vanilla-JS capture engine (no jQuery / underscore / js-cookie dependencies).
- Click-ID inference table (gclid, fbclid, msclkid, dclid, ttclid, li_fat_id, twclid, yclid,
  gclsrc, gsrc) configurable from the Settings tab.
- Explicit source/medium resolution cascade: explicit UTM → click-ID → referrer classification
  → direct. Respects user-defined alias order so `utm_` wins over custom conventions.
- Custom dimensions: admin can declare arbitrary tracked parameters (e.g. `match_type`)
  available as `{bw:custom.<key>}` merge tags.
- Gravity Forms integration: `{bw:source}`, `{bw:medium}`, `{bw:source_medium}`,
  `{bw:campaign}`, `{bw:term}`, `{bw:content}`, `{bw:adgroup}`, `{bw:first_page}`,
  `{bw:first_source}`, `{bw:first_medium}`, `{bw:visits}`, `{bw:pages}`,
  `{bw:tagged_visits}`, `{bw:summary}`, `{bw:custom.<key>}`. Tags are replaced client-side
  in hidden-field default values before submission.
- Admin **Test** tab: live inspection of storage, resolved merge tags, summary counters,
  visit history, and a URL simulator that opens any test URL in a new tab.
- Legacy CSS-selector form-field targeting retained in the **Form Fields** tab for non-GF
  form plugins.
- UTM Tracking URL builder retained (migrated from legacy plugin), cleaned up and rewritten
  in vanilla JS.
- Configurable source/medium separator (default ` / `) for the combined `{bw:source_medium}`
  merge tag.

### Changed
- Replaced self-hosted updater with the shared `plugin-update-checker` framework pointing at
  `plugins.bowden.works`.
- Option names moved from `bw_uac_settings` / `bw_utm_tracking` to `bw_lead_ai_settings` /
  `bw_lead_ai_utm_tracking`. No migration — this is a clean rewrite for new client sites.
- Storage key prefix changed from `bw_` to `bw_lai_` to avoid collision with any legacy
  install still present on the same origin.
- REST endpoint `/bw-lead-ai/v1/links` now requires `manage_options`; the legacy endpoint
  was publicly readable.
- IP address no longer included in the summary dump by default (privacy; was unauthenticated
  header-trusting in the legacy plugin).
- Referrer classification uses hostname matching, not substring-in-URL.

### Fixed
- `getParams()` silently dropped values when a URL parameter appeared more than once (bug in
  legacy `app.js`: referenced `value` instead of `val`).
- Multiple output paths in the admin UI lacked escaping and sanitization.
- CSS-selector injection risk in the hide-tracking-fields style block is now mitigated by a
  permissive allow-list filter.

## [0.1.0] - 2026-04-11

### Added
- Initial scaffold.
