From e3f439221a01523ba07113c8f3c44882962b5da4 Mon Sep 17 00:00:00 2001 From: justin Date: Tue, 23 Jun 2026 15:02:05 -0500 Subject: [PATCH] fix(trucking-email): kill recurring @TrackLink 404 at the source-clone boundary Root cause of the order-CTA 404s recurring after the prior live fix: the builder clones email bodies from STORED Listmonk source campaigns (ids 186/188/271-274/309/310/469/473), not from the edited source files. Those stored bodies still carried @TrackLink on the per-subscriber order CTA, so every nightly build re-registered a single static /order/&utm... link (no '?') that 404s for every recipient. This morning's 3,000 real sends AND the owner spot-check both went out with dead order links. Two durable guards: 1. get_base_campaign() now strips @TrackLink from any cloned body (with a warning), so a stale/re-edited source campaign can never reach recipients broken again. Human clicks are already attributed via Umami. 2. The owner test-send now builds the CTA via lp_link_with_coupon(dot=...) (leading '?') instead of build_lp_link() (bare path). Also fixed live: stripped @TrackLink from the 10 stored source campaign bodies; rewrote the 12 already-registered broken links. Backups in listmonk: pw_source_tracklink_bak_20260623 + pw_links_tracklink_bak_20260623. --- scripts/build_trucking_campaigns.py | 30 ++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/scripts/build_trucking_campaigns.py b/scripts/build_trucking_campaigns.py index a78c997..716a585 100644 --- a/scripts/build_trucking_campaigns.py +++ b/scripts/build_trucking_campaigns.py @@ -859,7 +859,21 @@ def listmonk_sendable(email: str) -> tuple[bool, str]: def get_base_campaign(campaign_id: int) -> dict: - return lm_api(f"/campaigns/{campaign_id}")["data"] + data = lm_api(f"/campaigns/{campaign_id}")["data"] + # Defensively strip Listmonk's @TrackLink marker from cloned bodies. Our CTAs + # are PER-SUBSCRIBER (`{{ lp_link }}`, `?dot=...`), and @TrackLink registers a + # SINGLE static URL per tracked link: it both 404s (the registered URL is + # captured with the `{{ lp_link }}` token unrendered -> `/order/slug&utm...` + # with no `?`) and collapses every recipient onto one carrier's redirect. + # Real human clicks are already attributed via Umami's campaign-click event, + # so dropping the marker loses nothing. This guard means a stale source + # campaign re-edited with @TrackLink can never reach recipients broken again. + body = data.get("body") + if body and "@TrackLink" in body: + data["body"] = body.replace("@TrackLink", "") + LOG.warning("[%s] stripped @TrackLink from cloned source body id=%s", + "get_base_campaign", campaign_id) + return data def create_list(name: str) -> int: @@ -1001,13 +1015,15 @@ def send_test(base: dict, campaign_id: int, sample_row: tuple, label: str, tz: s body = body.replace("{{ .Subscriber.Attribs.company }}", name or "Sample Carrier LLC") body = body.replace("{{ .Subscriber.Attribs.dot_number }}", dot or "0000000") body = body.replace("{{ .Subscriber.Attribs.state }}", state or "TX") - # Real subscribers get a populated lp_link attrib; the test send must mirror - # that or the CTA button (e.g. "Check My Emissions Status") renders as a bare - # "?dot=..." that links to nowhere. Build the same link the audience gets, - # using the target_state (the state the offer applies to, which for per-state - # programs comes from the deficiency flag, not the base state). + # Real subscribers get a populated lp_link attrib that already carries a + # leading `?` query (the carrier's `?dot=`, plus `?code=` when a coupon is + # on). The test send MUST mirror that: a bare path here would render the CTA + # as `/order/slug&utm_source=...` (the template appends `&utm...`) which has + # no `?` and 404s — exactly the broken owner-spot-check link. Use the same + # lp_link_with_coupon() the audience gets so the `?` is present. body = body.replace("{{ .Subscriber.Attribs.lp_link }}", - build_lp_link(campaign_type, target_state)) + lp_link_with_coupon(campaign_type, target_state, None, + dot=dot or "0000000")) # NOTE: leave {{ UnsubscribeURL }} alone — Listmonk renders it into a real, # working per-subscriber unsubscribe link (even on test sends). Overwriting it # produced a dead /unsubscribe link with no subscriber identity.