# BW Lead AI — 1.7.0 deploy + journey backfill

**Date:** 2026-08-07 · **Site:** secondwindconsultants.demoing.info

## What happened

1. Updated bw-lead-ai **1.4.0 → 1.7.0** from `plugins.bowden.works`.
2. Rebuilt **434 visitor journeys** from Gravity Forms entries that carried a
   `{bw:summary}`, backdated to the date each lead actually arrived.

## The result

| | |
|---|---|
| Journeys imported | **434** (saved) |
| GF entries carrying a summary | 435 active — 1 unparseable, reported not skipped silently |
| With page-by-page history | **408** (94%) |
| With a name and email | **434** (100%) |
| Unique people | **407** — so 27 enquired more than once |
| Date range | 2026-04-14 → 2026-08-05 |
| Spam entries | 37 found, **excluded** (they would have polluted every report) |

The summaries turned out to be in the **detailed** format, carrying `Tag Info` lines
and timestamped page URLs. That is why 94% have real per-visit browsing history
rather than only headline attribution — the reports have something substantial to
work with.

### Channel mix as imported

```
www.google.com                          107     chatgpt.com          22
Direct                                   87     www.bing.com          8
get.secondwindconsultants.demoing.info   83     Google Organic        8
Google Ads                               56     (unknown)            21
```

### The `www.google.com` question — not a live bug

The 107 records labelled `www.google.com` looked like a broken classification. They
are not. The month-by-month split settles it:

```
channel               2026-04  2026-05  2026-06  2026-07  2026-08
RAW www.google.com         23       41       25       19        0
Google Organic              0        0        0        4        4
RAW www.bing.com            2        3        2        1        0
Bing Organic                0        0        0        0        2
```

The raw hostnames stop dead and the proper channel names begin — exactly when
**1.2.1 (2026-07-26)** shipped the `hostMatches()` fix. This site was already on
1.4.0, so its live capture has been correct for weeks. The mislabelled records are
historical scars, frozen into summary text captured before the fix.

### What was done about it

The classification a summary captured is frozen at whatever the rules were that
day, so fixing the rules never reaches history. A `reclassify` mode was added to
the importer: it re-runs the site's *current* rules over the imported records,
touching only those still sitting in the `referral` catch-all (anything explicitly
UTM-tagged was never guessed at). Originals are preserved as `*_original` and the
raw summary is untouched, so it is reversible.

**183 of 434 records were corrected.** The preview caught a genuine bug before
anything was written: `mail.google.com` and `com.google.android.gm` were resolving
to **Google Organic** — Gmail counted as organic search, because the bare `google`
rule matches the `google` label inside both. An `email` rule was added above
`organic` in the plugin defaults and on this site, and only then was the
reclassification committed.

### Channel mix now

| Channel | Leads | Share |
|---|---:|---:|
| Google Organic | 119 | 27.4% |
| Direct | 88 | 20.3% |
| `get.` subdomain (self-referral) | 84 | 19.4% |
| Google Ads | 56 | 12.9% |
| **ChatGPT** | 22 | 5.1% |
| Social | 12 | 2.8% |
| Bing Organic | 10 | 2.3% |

**Google Organic is the single largest channel at 27.4%** — that was the truth
hidden behind the old labels, and it was previously showing as 8 leads.

**`get.secondwindconsultants.demoing.info` (84, ~19%) is still a self-referral**
from their own subdomain, counted as an acquisition channel. Left alone — deciding
whether that is internal traffic or a genuine property-to-property handoff is a
judgement about their business, not a data fix.

**17 records have no channel at all** (4%) and 6 more sit at `google / d` and
`google / g` — Google Ads visits whose medium came through as a single letter.
Both worth a look but neither is import damage; that is what the summary held.

## Verification done

- Record-by-record spot check against the Gravity Forms originals: channel,
  source/medium, first vs last touch, landing pages, counts, timeline and identity
  all match. Unresolved tag-manager placeholders (`{CampaignName}`) are correctly
  dropped rather than stored as campaign names.
- Backdating verified: `created_at` = `confirmed_at` = the GF entry date;
  `expires_at` = entry date + the site's 730-day retention (so 2028, nothing at
  risk of pruning, and the site's own policy is respected rather than overridden).
- **Idempotency proven** — a second run reports `imported: 0, already present: 434`.
- Journeys list and single-journey report both load and render correctly.
- Public site unaffected: no handoff script, no new front-end requests, pages
  respond normally through the id-auth gate.

## Changes made to the site

**Settings:** `capture_identity` turned on. This has no effect on live traffic here
— `BW_Lead_AI_Identity::register()` still requires cross-domain handoff, which is
off — it only controls whether the browse list shows its "Who" column.

**Three plugin files patched ahead of a release**, because a site like this one
could otherwise never see its own journeys:

| File | Change |
|---|---|
| `includes/class-bw-lead-ai-handoff-admin.php` | The Visitor Journeys menu appears when journeys exist, not only when cross-domain handoff is configured |
| `includes/class-bw-lead-ai-settings.php` | `capture_identity()` no longer requires cross-domain handoff |
| `tools/import-gf-history.php` | New — the importer itself |

These are the same insight as the Journeys/Cross-domain tab split: **journey
storage, identity and retention are not cross-domain concerns.** Before this,
`handoff_enabled()` required a destination domain *and* a mode, so a site whose
forms live on its own domain could never create the journey table or reach the
page. All three changes are staged in the dev plugin too and will be in the next
release; until then a plugin update to this site would revert them.

## Rollback

- Previous version: `/srv/apps/bw-plugins/.releases/bw-lead-ai/1.4.0/bw-lead-ai-1.4.0.zip`
- Settings before the update: `rollback/settings-before-1.7.0.json`
- To remove the imported journeys only:
  `DELETE FROM wp_bw_lead_ai_handoffs WHERE payload LIKE '%gf_entry_id%'`
  (every imported record carries `gf_entry_id`; nothing else is touched, and
  Gravity Forms was never written to at any point)

## Re-running the importer elsewhere

Any site that has been posting `{bw:summary}` into a form field can do this. Dry
run is the default and writes nothing:

```
wp eval-file wp-content/plugins/bw-lead-ai/tools/import-gf-history.php
wp eval-file wp-content/plugins/bw-lead-ai/tools/import-gf-history.php commit
```

Arguments are positional (`commit`, `limit=50`, `include-spam`) — wp-cli consumes
anything that looks like a flag before the script sees it.
