fix(hc-cron): stop re-mailing the whole list daily (per-day send lists)
The HC warmup imported ~1000 fresh providers/day into a persistent segment list (list 10), but each day's campaign targeted that WHOLE cumulative list -- so the cohort imported on day 1 received the identical 'Free NPI Check' email every subsequent day (verified: subscriber 3410 got campaigns 38-42, 5x). Program-wide that was 23,843 sends to 6,587 people (3.6x avg, max 30x) and blocklisted ~12% of the list -- a Yahoo 'user complaints' deferral now confirms the burn. Fix: import each day's slice into a dedicated dated SEND list and bind that day's campaign to ONLY that list, so every provider gets exactly one send. The persistent segment list is still used for dedup/records. ensure_campaign now matches the exact dated name (never reuses a prior day's campaign/slice).
This commit is contained in:
parent
4276adab80
commit
b350a1367d
1 changed files with 30 additions and 10 deletions
|
|
@ -301,20 +301,23 @@ def add_subscriber(list_id: int, email: str, name: str, attribs: dict) -> bool:
|
|||
return False
|
||||
|
||||
|
||||
def ensure_campaign(seg_key: str, list_id: int) -> int:
|
||||
# Reuse an existing warmup campaign for this segment only if it's still
|
||||
# ACTIVE (draft / running / paused / scheduled). A finished/cancelled one
|
||||
# can't accept new subscribers or restart, so we create a fresh dated one --
|
||||
# that also picks up the latest email template (the canonical hc_*.html).
|
||||
def ensure_campaign(seg_key: str, list_id: int, send_list_name: str | None = None) -> int:
|
||||
# One campaign per (segment, day): each day's campaign is bound to that day's
|
||||
# dedicated SEND list, so it mails ONLY that day's newly-imported slice. We
|
||||
# therefore reuse a campaign only when it's the SAME day's campaign (matched by
|
||||
# the dated name) and still active; we never reuse a prior day's campaign,
|
||||
# because that would re-target an old slice. A finished/cancelled same-day
|
||||
# campaign can't restart, so a fresh one is created (also picking up the latest
|
||||
# canonical hc_*.html template).
|
||||
from datetime import date
|
||||
seg = SEGMENTS[seg_key]
|
||||
ACTIVE = {"draft", "running", "paused", "scheduled"}
|
||||
dated = f"{seg['campaign_name']} - {date.today():%b %d %Y}"
|
||||
res = lm("/campaigns?per_page=100")
|
||||
for c in res.get("data", {}).get("results", []):
|
||||
if c["name"].startswith(seg["campaign_name"]) and c.get("status") in ACTIVE:
|
||||
if c["name"] == dated and c.get("status") in ACTIVE:
|
||||
return c["id"]
|
||||
body = open(template_path(seg_key)).read()
|
||||
dated = f"{seg['campaign_name']} - {date.today():%b %d %Y}"
|
||||
payload = {
|
||||
"name": dated, "subject": seg["subject"], "lists": [list_id],
|
||||
"from_email": FROM_EMAIL, "type": "regular", "content_type": "richtext",
|
||||
|
|
@ -552,13 +555,29 @@ def warm_segment(seg_key: str, rows: list[dict], slice_n: int,
|
|||
return 0
|
||||
|
||||
list_id = get_or_create_list(seg["list_name"], ["healthcare", "warmup", seg_key])
|
||||
# Per-day SEND list: the campaign targets ONLY today's newly-imported slice,
|
||||
# never the whole accumulated segment list. Without this, each day's campaign
|
||||
# re-mailed every prior subscriber (the cumulative list), so the cohort
|
||||
# imported on day 1 received the identical email every subsequent day -- a
|
||||
# 5-6x repeat that burned reputation and blocklisted ~12% of the list. The
|
||||
# persistent `list_id` is still used for dedup/records; the dated send list is
|
||||
# what the campaign actually sends to, so every provider gets exactly one send.
|
||||
from datetime import date
|
||||
send_list_name = f"{seg['list_name']} - SEND {date.today():%Y-%m-%d}"
|
||||
send_list_id = get_or_create_list(send_list_name,
|
||||
["healthcare", "warmup", seg_key, "daily-send"])
|
||||
n_ok = 0
|
||||
for r in todo:
|
||||
email = r["email"].strip().lower()
|
||||
if add_subscriber(list_id, email, r.get("name") or "", attribs_for(r)):
|
||||
# Add to BOTH the persistent segment list (dedup/records) and today's
|
||||
# send list (the campaign's only audience).
|
||||
ok = add_subscriber(list_id, email, r.get("name") or "", attribs_for(r))
|
||||
add_subscriber(send_list_id, email, r.get("name") or "", attribs_for(r))
|
||||
if ok:
|
||||
imported.add(email); n_ok += 1
|
||||
save_imported(seg_key, imported)
|
||||
cid = ensure_campaign(seg_key, list_id)
|
||||
# Campaign targets the per-day send list, NOT the cumulative segment list.
|
||||
cid = ensure_campaign(seg_key, send_list_id, send_list_name)
|
||||
if start_campaign:
|
||||
try:
|
||||
lm(f"/campaigns/{cid}/status", {"status": "running"}, "PUT")
|
||||
|
|
@ -566,7 +585,8 @@ def warm_segment(seg_key: str, rows: list[dict], slice_n: int,
|
|||
# Already running raises; that's fine.
|
||||
if "already" not in str(e).lower():
|
||||
print(f"[hc-cron] {seg_key}: start warning: {e}")
|
||||
print(f"[hc-cron] {seg_key}: imported {n_ok} into list {list_id}; campaign={cid}")
|
||||
print(f"[hc-cron] {seg_key}: imported {n_ok} into list {list_id} "
|
||||
f"(send list {send_list_id}); campaign={cid}")
|
||||
return n_ok
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue