diff --git a/data/hc_campaigns/hc_clia_renewal.html b/data/hc_campaigns/hc_clia_renewal.html index b62fb83..d943f3c 100644 --- a/data/hc_campaigns/hc_clia_renewal.html +++ b/data/hc_campaigns/hc_clia_renewal.html @@ -58,7 +58,7 @@

Let us take the CLIA renewal off your plate — the sooner we start, the better.

We submit most filings within 1-2 business days, then track it through CMS processing to confirmation.

- Renew my CLIA certificate → + Renew my CLIA certificate →
diff --git a/data/hc_campaigns/hc_compliance_bundle.html b/data/hc_campaigns/hc_compliance_bundle.html index c235c36..63a1e84 100644 --- a/data/hc_campaigns/hc_compliance_bundle.html +++ b/data/hc_campaigns/hc_compliance_bundle.html @@ -42,7 +42,7 @@

One annual bundle covers your core CMS obligations.

We watch the deadlines so you never miss one.

- Get the compliance bundle → + Get the compliance bundle →
diff --git a/data/hc_campaigns/hc_npi_reactivation.html b/data/hc_campaigns/hc_npi_reactivation.html index dbc6ea7..3555e18 100644 --- a/data/hc_campaigns/hc_npi_reactivation.html +++ b/data/hc_campaigns/hc_npi_reactivation.html @@ -62,7 +62,7 @@

We handle the CMS-855 reactivation end to end.

We verify every field against current CMS requirements.

- Reactivate my enrollment → + Reactivate my enrollment →
diff --git a/data/hc_campaigns/hc_nppes_outdated.html b/data/hc_campaigns/hc_nppes_outdated.html index 533be90..d0766b4 100644 --- a/data/hc_campaigns/hc_nppes_outdated.html +++ b/data/hc_campaigns/hc_nppes_outdated.html @@ -62,7 +62,7 @@

Run your free compliance check — takes about 30 seconds.

Your NPI is pre-filled. No signup, no cost — just your results.

- Run my free NPI check → + Run my free NPI check →
diff --git a/data/hc_campaigns/hc_oig_screening.html b/data/hc_campaigns/hc_oig_screening.html index 5c729e7..2b549d2 100644 --- a/data/hc_campaigns/hc_oig_screening.html +++ b/data/hc_campaigns/hc_oig_screening.html @@ -42,7 +42,7 @@

We run and document your OIG/SAM exclusion screening.

Monthly checks with an audit-ready record — cancel anytime.

- Set up exclusion screening → + Set up exclusion screening →
diff --git a/data/hc_campaigns/hc_revalidation_due_soon.html b/data/hc_campaigns/hc_revalidation_due_soon.html index db476bc..c53bee1 100644 --- a/data/hc_campaigns/hc_revalidation_due_soon.html +++ b/data/hc_campaigns/hc_revalidation_due_soon.html @@ -66,7 +66,7 @@

We file your PECOS revalidation for you, well before the deadline.

Most filings submitted within 1-2 business days.

- Start my revalidation → + Start my revalidation →
diff --git a/data/hc_campaigns/hc_revalidation_overdue.html b/data/hc_campaigns/hc_revalidation_overdue.html index 9ecea5b..7ae14f5 100644 --- a/data/hc_campaigns/hc_revalidation_overdue.html +++ b/data/hc_campaigns/hc_revalidation_overdue.html @@ -66,7 +66,7 @@

We file your PECOS revalidation for you, before the clock runs out.

Most filings submitted within 1-2 business days.

- Start my revalidation → + Start my revalidation →
diff --git a/data/hc_campaigns/hc_revalidation_overdue_personal.html b/data/hc_campaigns/hc_revalidation_overdue_personal.html index 0391951..0d5d67c 100644 --- a/data/hc_campaigns/hc_revalidation_overdue_personal.html +++ b/data/hc_campaigns/hc_revalidation_overdue_personal.html @@ -58,7 +58,7 @@

Let us get your past-due revalidation filed right away.

We submit most filings within 1-2 business days, then track it through CMS processing to confirmation.

- Handle my revalidation → + Handle my revalidation →
diff --git a/data/hc_campaigns/hc_revalidation_personal.html b/data/hc_campaigns/hc_revalidation_personal.html index b60b497..10bd8ee 100644 --- a/data/hc_campaigns/hc_revalidation_personal.html +++ b/data/hc_campaigns/hc_revalidation_personal.html @@ -57,7 +57,7 @@

Let us take revalidation off your plate — the sooner we start, the better.

We submit most filings within 1-2 business days, then track it through CMS processing to confirmation.

- Handle my revalidation → + Handle my revalidation →
diff --git a/data/hc_campaigns/hc_revalidation_turnover.html b/data/hc_campaigns/hc_revalidation_turnover.html index 0d4c7cc..3a88334 100644 --- a/data/hc_campaigns/hc_revalidation_turnover.html +++ b/data/hc_campaigns/hc_revalidation_turnover.html @@ -57,7 +57,7 @@

Let us take revalidation off your plate — the sooner we start, the better.

We submit most filings within 1-2 business days, then track it through CMS processing to confirmation.

- Handle my revalidation → + Handle my revalidation →
diff --git a/data/trucking_campaigns/ifta_quarterly_reminder.html b/data/trucking_campaigns/ifta_quarterly_reminder.html index ab3a084..6ed2488 100644 --- a/data/trucking_campaigns/ifta_quarterly_reminder.html +++ b/data/trucking_campaigns/ifta_quarterly_reminder.html @@ -30,7 +30,7 @@

Send us your total miles and gallons by jurisdiction for the quarter. We calculate the tax owed for every state you ran in, prepare the return, and file it. You just review and we handle the rest - so you can get back to driving.

Or call us directly at (888) 411-0383.

diff --git a/data/trucking_campaigns/ucr_annual_reminder.html b/data/trucking_campaigns/ucr_annual_reminder.html index 67c3ad1..a1302b5 100644 --- a/data/trucking_campaigns/ucr_annual_reminder.html +++ b/data/trucking_campaigns/ucr_annual_reminder.html @@ -30,7 +30,7 @@

UCR fees are based on your fleet size, and getting the tier wrong causes rejections and delays. Tell us your power-unit count and we file it correctly the first time, so you stay legal and on the road.

Or call us directly at (888) 411-0383.

diff --git a/docs/email-deliverability-runbook.md b/docs/email-deliverability-runbook.md index e544f5f..c43ca29 100644 --- a/docs/email-deliverability-runbook.md +++ b/docs/email-deliverability-runbook.md @@ -201,6 +201,65 @@ b.old_listmonk_sent_at FROM resend_dkim_backup_20260622 b WHERE c.dot_number = b.dot_number;`. To resume normal warmup exclusion later, unset `MAIN_EXCLUDE_OPERATORS` (reverts to Google+Microsoft+consumer-MX held to day 30). +### Incident: Jun 22 2026 — `@TrackLink` on per-subscriber CTAs = 404 + collapse + +**Symptom.** The trucking "deficiency" CTA buttons (the primary order link and the +secondary DOT-check link) rendered as Listmonk tracking redirects +(`https://lists.performancewest.net/link//...`) that **404'd**. The redirect +target (registered in `links.url`) was `https://performancewest.net/order/boc3-filing&utm_source=...` +— note the `&` with **no `?`** — an invalid URL. + +**Root cause.** Listmonk's `@TrackLink` marker registers **one static URL per +tracked link** and points every recipient's `/link/` redirect at that single +row. This is fundamentally incompatible with a **per-subscriber** href such as +`{{ .Subscriber.Attribs.lp_link }}&utm_source=...`: +- The registered `links.url` was captured with the `{{ lp_link }}` token dropped, + yielding `/order/slug&utm_source=...` (first `&`, no `?`) → **404 for everyone**. +- Even if the URL had been valid, a static registration **collapses every carrier + onto the first subscriber's** `?dot=` (or `?npi=`/`?clia=`) value — wrong order + pre-fill for the entire blast. + +By contrast, a **static** CTA (same URL for all recipients, e.g. the CRTC +`?code=...` order link) tracks correctly — keep `@TrackLink` there. + +**Why removing tracking loses nothing.** Real human clicks are already attributed +via Umami's `campaign-click` event (bot-filtered by `pw-bot-filter.js`). Listmonk's +own click counters were already established as unreliable for this stream. So +Listmonk link tracking on per-subscriber CTAs is both redundant and destructive. + +**Fix — live (already-sent + in-flight mail).** Rewrote the 10 broken registered +rows in place (replace the first `&` with `?`) so the baked `/link/` redirects +resolve. Backup table `listmonk.pw_links_dkim_fix_bak_20260622` holds the old urls. +Verified the exact redirect that 404'd now returns 200 → lands on the (generic, +DOT-not-prefilled but fully functional) order page. To revert: +```sql +UPDATE links l SET url = b.url +FROM pw_links_dkim_fix_bak_20260622 b WHERE l.id = b.id; -- in the `listmonk` DB +``` + +**Fix — source (future builds, the real fix).** Stripped `@TrackLink` from every +**per-subscriber / per-provider** CTA so each row renders its own direct link (no +redirect, no collapse). Files changed: +- `scripts/create_deficiency_source_campaigns.py` — `_cta()` (lp_link order button) + and `_dot_check_cta()` (per-DOT tools link). +- `data/trucking_campaigns/{ucr_annual_reminder,ifta_quarterly_reminder}.html` + (per-carrier `lp_link`). +- `data/hc_campaigns/*.html` (10 templates, per-provider `?npi=`/`?clia=`). +`lp_link` already starts its query with `?dot=` (see `lp_link_with_coupon()`), so +`{{ lp_link }}&utm...` renders to a valid per-carrier URL once the redirect is gone. + +**Healthcare note.** The HC Listmonk DB (`listmonk_hc`) had **0 registered links** +despite 13,425 sent — `@TrackLink` was not being stripped there at all, so the +literal `@TrackLink` shipped as harmless trailing text in `utm_campaign` and the +hrefs still 200'd (per-provider `?npi=` was present literally in the template, not +via lp_link). No live HC breakage; source templates cleaned anyway to remove the +collapse risk on the next send. + +**Guardrail.** Never put `@TrackLink` on an href containing a `{{ .Subscriber... }}` +token. Per-subscriber links must render directly; rely on Umami `campaign-click` +for human-click attribution. + + ### Follow-up hardening — DONE (Jun 17-18 2026) All discovered during the post-incident technical audit; each fix is codified. diff --git a/docs/fmcsa-trucking-plan.md b/docs/fmcsa-trucking-plan.md index 3b20548..dd86a80 100644 --- a/docs/fmcsa-trucking-plan.md +++ b/docs/fmcsa-trucking-plan.md @@ -137,7 +137,11 @@ - Same Listmonk infrastructure as FCC campaigns - Warmup schedule (200/day → ramp up) - Link to DOT compliance checker with `?dot={DOT#}&email={email}` pre-filled -- Use `@TrackLink` on all CTAs (learned from FCC campaign mistake) +- Use `@TrackLink` ONLY on **static** CTAs (same URL for all recipients). NEVER on + a per-subscriber href such as `{{ lp_link }}` / `?dot={DOT#}` — Listmonk registers + one static URL per tracked link and would 404 + collapse every carrier onto one + DOT (see runbook "Jun 22 2026 — @TrackLink on per-subscriber CTAs"). Per-subscriber + links render directly; human clicks are tracked via Umami `campaign-click`. - Free compliance check as the CTA (not direct sell) ## Phase 5: Automation (Future) diff --git a/docs/healthcare-email-compliance-review.md b/docs/healthcare-email-compliance-review.md index 4aeca4d..18e0829 100644 --- a/docs/healthcare-email-compliance-review.md +++ b/docs/healthcare-email-compliance-review.md @@ -7,8 +7,13 @@ click tracking, and de-risking unsubstantiated status claims. 1. **Removed all service prices** from the emails (price is now revealed on the order page, after value is established). Catalog (`api/src/service-catalog.ts`) remains the source of truth. -2. **Fixed click tracking** — appended `@TrackLink` + UTM to every conversion CTA - (root cause of clicks=0; Listmonk only registers links with that marker). +2. **Click tracking** — originally appended `@TrackLink` + UTM to every conversion + CTA. **SUPERSEDED (Jun 22 2026):** `@TrackLink` must NOT be used on per-provider + hrefs (`?npi=`/`?clia=`/`{{ lp_link }}`) — Listmonk registers one static URL per + tracked link, which 404s and collapses every provider onto one NPI. `@TrackLink` + removed from all HC templates; per-provider links render directly and human clicks + are tracked via Umami `campaign-click`. See runbook "Jun 22 2026 — @TrackLink on + per-subscriber CTAs." 3. **Reframed unsubstantiated per-record status assertions** to honest, hedged, generally-true statements (defamation / FTC-deception risk). 4. This compliance review. @@ -84,8 +89,9 @@ These are factual compliance claims and must be **literally true**: ## HTML / deliverability QA — PASS - All 10 templates render with **0 JS errors** headless, each has **exactly one - tracked `/order/...@TrackLink` CTA**, and **no price leaks** (only the $20,000 - OIG penalty stat remains, intentionally). + per-provider `/order/...` CTA** (direct link, `@TrackLink` removed Jun 22 2026 — + see item 2), and **no price leaks** (only the $20,000 OIG penalty stat remains, + intentionally). - External self-verify links (oig.hhs.gov, sam.gov, npiregistry, data.cms.gov) left **untracked** on purpose (they're trust links, not conversions). diff --git a/scripts/create_deficiency_source_campaigns.py b/scripts/create_deficiency_source_campaigns.py index a232d37..c8550bf 100644 --- a/scripts/create_deficiency_source_campaigns.py +++ b/scripts/create_deficiency_source_campaigns.py @@ -128,10 +128,20 @@ def _cta(label): # so the template appends its own params with a leading `&` — correct whether # or not the coupon is on. (Previously this used `?dot=`, which double-`?`d # the URL once the coupon added its own query.) + # + # NO `@TrackLink` here: Listmonk registers a *single static URL per tracked + # link* and points every recipient's /link/ redirect at it. For a + # per-subscriber URL like lp_link that is doubly broken — (1) the registered + # URL was captured before the `{{ lp_link }}` token rendered, dropping the + # `?dot=` and producing `/order/slug&utm_source=...` (no `?`) which 404s, and + # (2) even when valid it collapses EVERY carrier onto the first subscriber's + # DOT. Real human clicks are tracked via Umami's `campaign-click` event + # (bot-filtered), so Listmonk link tracking is redundant here. Rendering the + # link directly gives each carrier their own correct `?dot=` URL, no redirect. return ( '' ) @@ -149,7 +159,7 @@ def _dot_check_cta(): '

' 'Want to verify everything else on your DOT profile first?

' f'' 'Run Free DOT Compliance Check →
'