# BW Lead Attribution Intelligence — Specification

**Version:** 0.6.0 | **Last Updated:** 2026-04-12

## Purpose

Capture the traffic source of every website visitor and attach it to the leads they generate,
so agency clients can see — in their CRM — exactly which campaigns, keywords, and landing
pages produce real inquiries.

The plugin solves three specific problems marketers hit in practice:

1. **Submissions don't happen on landing pages.** Users land on a campaign page, browse, and
   submit on a different page (sometimes after leaving and returning). UTM parameters only
   exist on the landing URL, so without stored state the form captures nothing.
2. **Google Ads auto-tagging replaces UTM with `gclid`.** The campaign/keyword/etc. aren't in
   the URL at all. The plugin infers what it can (`gclid` → `google / cpc`) and treats
   valuetrack parameters or custom conventions (`ctm_*`) as additional signal.
3. **Conflicts between tagging conventions.** UTM + custom + click-ID may all be present. The
   plugin has an explicit resolution cascade and lets the admin tune it.

## Requirements (Phase 1)

### Functional

- **R1**: Capture source, medium, campaign, term, content, adgroup from configurable URL
  parameter aliases (e.g. `utm_source`, `source`, `src`, `ctm_source`).
- **R2**: Infer source/medium from click-ID parameters (`gclid`, `fbclid`, `msclkid`, `dclid`,
  `ttclid`, `li_fat_id`, `twclid`, `yclid`, `gclsrc`) when explicit UTMs are absent. Table is
  configurable.
- **R3**: Classify organic-search and social referrers by hostname match against configurable
  lists. Treat unknown external referrers as `referral`. Treat no referrer as `(direct)`.
- **R4**: Store the first visit (`original`), the latest visit (`last`), a running summary
  (visit / page / tagged / usedPaid counters), and a capped visit history (first 5 + last 5)
  in `localStorage` with a `sessionStorage` session marker.
- **R5**: Support arbitrary **custom dimensions** declared via the admin UI (e.g.
  `match_type|mt,match,utm_match_type`). Resolved per visit, stored on the last/original
  visit record, and exposed as merge tags.
- **R6**: Provide Gravity Forms merge tags (`{bw:source}`, `{bw:medium}`, `{bw:source_medium}`,
  `{bw:campaign}`, `{bw:term}`, `{bw:content}`, `{bw:adgroup}`, `{bw:first_page}`,
  `{bw:last_page}`, `{bw:submit_page}`, `{bw:first_source}`, `{bw:first_medium}`, `{bw:visits}`, `{bw:pages}`, `{bw:tagged_visits}`,
  `{bw:summary}`, `{bw:custom.<key>}`). Register with GF server-side; substitute client-side
  at form load and form submit so dynamic fields work.
- **R7**: Provide legacy CSS-selector / id / class / name field targeting for non-GF setups,
  for the fields: summary, source, medium, sources-list, terms-list, first-page.
- **R8**: Provide a UTM URL builder tab (retained from legacy) with live preview of the
  tracked URL and copy-to-clipboard.
- **R9**: Provide an admin **Test** tab showing current browser state, resolved merge tags,
  summary counters, visit history, raw storage, a URL simulator (open in new tab), and a
  "clear all tracking storage" button.
- **R10**: Support configurable separator for `{bw:source_medium}` (default ` / `).
- **R11**: Fall back to cookies when `localStorage` is unavailable (degraded mode: only the
  session marker and one-off reads work; history is not stored).
- **R12**: Respect a hard resolution cascade (highest priority first):
    1. Explicit source **and** medium from URL alias lists → use both.
    2. Explicit source only → use it; fill medium from click-ID inference or `referral`.
    3. Click-ID match only → use click-ID's source/medium pair.
    4. Referrer hostname classification → organic / social / referral.
    5. Direct → `(direct)` / `(none)`.
   Admin can tune alias order (UTM before custom or vice versa) by editing the alias lists.

### Non-functional

- WordPress 6.0+, PHP 7.4+.
- No dependencies on jQuery or any third-party JS library.
- Zero server-side REST calls from the front-end.
- All options in a single `bw_lead_ai_settings` row (+ `bw_lead_ai_utm_tracking` for the
  builder).
- Passes `cleanup-scan`, `security-scan`, `test-plugin`.
- Translatable via `load_plugin_textdomain()`.

## Acceptance Criteria

| Requirement | Acceptance | Status |
|---|---|---|
| R1 | URL `?utm_source=x&utm_medium=y` sets last.source=x, last.medium=y | ☐ pending manual test |
| R2 | URL `?gclid=abc` with no UTMs sets source=google, medium=cpc | ☐ |
| R3 | Referrer `https://www.google.com/search?q=...` sets source=google, medium=organic | ☐ |
| R4 | Visiting 12 different landing URLs across sessions leaves exactly 10 stored visits | ☐ |
| R5 | Custom dimension `match_type|mt` → URL `?mt=exact` makes `{bw:custom.match_type}`=exact | ☐ |
| R6 | A GF hidden field with default `{bw:source_medium}` submits `google / cpc` | ☐ |
| R7 | Legacy target `name=lead_source` receives the last source value | ☐ |
| R8 | UTM builder updates tracked URL live as fields change | ☐ |
| R9 | Test tab shows identical state to what the front-end capture wrote | ☐ |
| R12 | With `?utm_source=facebook&gclid=abc`, source=facebook wins (UTM priority) | ☐ |

## Out of Scope (Phase 1)

- Auto-migration from the legacy `bw-user-analytics` plugin (no clients to migrate).
- A dedicated "combined source / medium" form field target — GF merge tags make this a
  no-op: one hidden field with default `{bw:source} / {bw:medium}` covers it.
- WP admin reporting dashboard (Phase 2).
- Live visitor tracking (Phase 3).
- Non-GF form plugin integrations beyond the legacy selector-based targets.
