report: daily price A/B/C scoreboard to Telegram (silent on no-sale days)
Adds --telegram mode to coupon_ab_scoreboard.py + a daily cron (18:00 UTC). Pushes the per-arm clicks+conversions table to Telegram ONLY when there was at least one PAID sale in the window; stays silent otherwise (per request, no all-zero spam). Creds read from /opt/performancewest/.env like the other monitors. Verified live: silent at 0 sales (14d), sends at 5 sales (30d).
This commit is contained in:
parent
60d5d3b9d8
commit
9abd08c305
2 changed files with 82 additions and 11 deletions
6
infra/cron/pw-coupon-ab-report
Normal file
6
infra/cron/pw-coupon-ab-report
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Daily trucking price A/B/C scoreboard -> Telegram, but ONLY when there was at
|
||||
# least one PAID sale in the look-back window (silent on no-sale days, per
|
||||
# request). Combines HUMAN clicks (Umami, bot-filtered) + PAID orders per arm.
|
||||
# Runs as deploy (needs docker exec for psql + reads /opt/performancewest/.env).
|
||||
# 18:00 UTC (1pm CT) so the day's morning sends + any same-day orders are in.
|
||||
0 18 * * * deploy python3 /usr/local/bin/coupon_ab_scoreboard.py --days 14 --telegram --arms 20,30,40,0 >> /opt/performancewest/logs/pw-coupon-ab-report.log 2>&1
|
||||
|
|
@ -31,6 +31,8 @@ import json
|
|||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from collections import defaultdict
|
||||
|
||||
# How to reach the two databases. On the host we exec into the postgres
|
||||
|
|
@ -67,11 +69,47 @@ def arm_for_email(email: str, arms: list[int]) -> int:
|
|||
return pcts[int(h, 16) % len(pcts)]
|
||||
|
||||
|
||||
def send_telegram(text: str) -> bool:
|
||||
"""Post a message to the ops Telegram chat. Creds from /opt/performancewest/.env
|
||||
(TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID), same as the other monitors. Returns
|
||||
True if a request was attempted with creds present.
|
||||
"""
|
||||
env_path = os.getenv("PW_ENV_FILE", "/opt/performancewest/.env")
|
||||
bot = chat = ""
|
||||
try:
|
||||
with open(env_path) as f:
|
||||
for line in f:
|
||||
if line.startswith("TELEGRAM_BOT_TOKEN="):
|
||||
bot = line.split("=", 1)[1].strip()
|
||||
elif line.startswith("TELEGRAM_CHAT_ID="):
|
||||
chat = line.split("=", 1)[1].strip()
|
||||
except OSError:
|
||||
pass
|
||||
bot = os.getenv("TELEGRAM_BOT_TOKEN", bot)
|
||||
chat = os.getenv("TELEGRAM_CHAT_ID", chat)
|
||||
if not bot or not chat:
|
||||
return False
|
||||
data = urllib.parse.urlencode(
|
||||
{"chat_id": chat, "text": text, "parse_mode": "HTML"}
|
||||
).encode()
|
||||
req = urllib.request.Request(
|
||||
f"https://api.telegram.org/bot{bot}/sendMessage", data=data
|
||||
)
|
||||
try:
|
||||
urllib.request.urlopen(req, timeout=10).read()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--days", type=int, default=14)
|
||||
ap.add_argument("--arms", default=os.getenv("CAMPAIGN_COUPON_AB_PCTS", "20,30,40,0"),
|
||||
help="comma list matching CAMPAIGN_COUPON_AB_PCTS, e.g. 20,30,40,0")
|
||||
ap.add_argument("--telegram", action="store_true",
|
||||
help="push the scoreboard to Telegram, but ONLY when there is >=1 paid sale in the window")
|
||||
args = ap.parse_args()
|
||||
|
||||
arms = [int(a) for a in args.arms.split(",") if a.strip().lstrip("-").isdigit()]
|
||||
|
|
@ -140,23 +178,50 @@ def main() -> int:
|
|||
paid[arm] += 1
|
||||
revenue[arm] += fee_cents
|
||||
|
||||
# ── 4. Print the scoreboard ──────────────────────────────────────────────────
|
||||
print(f"\n=== Trucking price A/B/C scoreboard (last {days} days) ===")
|
||||
print(f"arms: {arms} (control = full price, no code)\n")
|
||||
hdr = f"{'arm':>8} {'human_clicks':>13} {'paid_orders':>12} {'revenue':>10} {'click→order':>12}"
|
||||
print(hdr)
|
||||
print("-" * len(hdr))
|
||||
# ── 4. Build the scoreboard rows ─────────────────────────────────────────────
|
||||
tot_c = sum(clicks.values())
|
||||
tot_o = sum(paid.values())
|
||||
tot_r = sum(revenue.values()) / 100.0
|
||||
|
||||
hdr = f"{'arm':>8} {'human_clicks':>13} {'paid_orders':>12} {'revenue':>10} {'click->order':>12}"
|
||||
lines = [
|
||||
f"=== Trucking price A/B/C scoreboard (last {days} days) ===",
|
||||
f"arms: {arms} (control = full price, no code)",
|
||||
"",
|
||||
hdr,
|
||||
"-" * len(hdr),
|
||||
]
|
||||
for pct in sorted(arms):
|
||||
label = f"{pct}% off" if pct > 0 else "control"
|
||||
c = clicks.get(pct, 0)
|
||||
o = paid.get(pct, 0)
|
||||
rev = revenue.get(pct, 0) / 100.0
|
||||
cvr = f"{(o / c * 100):.1f}%" if c else "-"
|
||||
print(f"{label:>8} {c:>13} {o:>12} {('$%.0f' % rev):>10} {cvr:>12}")
|
||||
tot_c = sum(clicks.values()); tot_o = sum(paid.values()); tot_r = sum(revenue.values()) / 100.0
|
||||
print("-" * len(hdr))
|
||||
print(f"{'TOTAL':>8} {tot_c:>13} {tot_o:>12} {('$%.0f' % tot_r):>10} "
|
||||
f"{((tot_o/tot_c*100) if tot_c else 0):>11.1f}%")
|
||||
lines.append(f"{label:>8} {c:>13} {o:>12} {('$%.0f' % rev):>10} {cvr:>12}")
|
||||
lines.append("-" * len(hdr))
|
||||
lines.append(f"{'TOTAL':>8} {tot_c:>13} {tot_o:>12} {('$%.0f' % tot_r):>10} "
|
||||
f"{((tot_o/tot_c*100) if tot_c else 0):>11.1f}%")
|
||||
table = "\n".join(lines)
|
||||
|
||||
if args.telegram:
|
||||
# Daily push, but ONLY when there is at least one paid sale in the window.
|
||||
# A discount price test is only meaningful once money moves; on no-sale days
|
||||
# we stay silent (per request) rather than spam an all-zero table.
|
||||
if tot_o <= 0:
|
||||
print(table)
|
||||
print("\n[telegram] no paid sales in window — staying silent (no report sent).")
|
||||
return 0
|
||||
msg = (f"Trucking price A/B/C ({days}d)\n"
|
||||
f"{tot_o} paid / ${tot_r:.0f} rev / {tot_c} human clicks\n\n"
|
||||
f"<pre>{table}</pre>")
|
||||
sent = send_telegram(msg)
|
||||
print(table)
|
||||
print(f"\n[telegram] {'sent' if sent else 'NOT sent (missing creds?)'} "
|
||||
f"({tot_o} paid sales triggered the report).")
|
||||
return 0
|
||||
|
||||
# ── Console output ───────────────────────────────────────────────────────────
|
||||
print(table)
|
||||
print("\nClicks are HUMAN (bot-filtered via Umami before-send). Conversions are")
|
||||
print("PAID Stripe orders. Control conversions recovered by email re-hash.")
|
||||
if tot_o < 30:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue