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
|
return False
|
||||||
|
|
||||||
|
|
||||||
def ensure_campaign(seg_key: str, list_id: int) -> int:
|
def ensure_campaign(seg_key: str, list_id: int, send_list_name: str | None = None) -> int:
|
||||||
# Reuse an existing warmup campaign for this segment only if it's still
|
# One campaign per (segment, day): each day's campaign is bound to that day's
|
||||||
# ACTIVE (draft / running / paused / scheduled). A finished/cancelled one
|
# dedicated SEND list, so it mails ONLY that day's newly-imported slice. We
|
||||||
# can't accept new subscribers or restart, so we create a fresh dated one --
|
# therefore reuse a campaign only when it's the SAME day's campaign (matched by
|
||||||
# that also picks up the latest email template (the canonical hc_*.html).
|
# 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
|
from datetime import date
|
||||||
seg = SEGMENTS[seg_key]
|
seg = SEGMENTS[seg_key]
|
||||||
ACTIVE = {"draft", "running", "paused", "scheduled"}
|
ACTIVE = {"draft", "running", "paused", "scheduled"}
|
||||||
|
dated = f"{seg['campaign_name']} - {date.today():%b %d %Y}"
|
||||||
res = lm("/campaigns?per_page=100")
|
res = lm("/campaigns?per_page=100")
|
||||||
for c in res.get("data", {}).get("results", []):
|
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"]
|
return c["id"]
|
||||||
body = open(template_path(seg_key)).read()
|
body = open(template_path(seg_key)).read()
|
||||||
dated = f"{seg['campaign_name']} - {date.today():%b %d %Y}"
|
|
||||||
payload = {
|
payload = {
|
||||||
"name": dated, "subject": seg["subject"], "lists": [list_id],
|
"name": dated, "subject": seg["subject"], "lists": [list_id],
|
||||||
"from_email": FROM_EMAIL, "type": "regular", "content_type": "richtext",
|
"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
|
return 0
|
||||||
|
|
||||||
list_id = get_or_create_list(seg["list_name"], ["healthcare", "warmup", seg_key])
|
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
|
n_ok = 0
|
||||||
for r in todo:
|
for r in todo:
|
||||||
email = r["email"].strip().lower()
|
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
|
imported.add(email); n_ok += 1
|
||||||
save_imported(seg_key, imported)
|
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:
|
if start_campaign:
|
||||||
try:
|
try:
|
||||||
lm(f"/campaigns/{cid}/status", {"status": "running"}, "PUT")
|
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.
|
# Already running raises; that's fine.
|
||||||
if "already" not in str(e).lower():
|
if "already" not in str(e).lower():
|
||||||
print(f"[hc-cron] {seg_key}: start warning: {e}")
|
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
|
return n_ok
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue