From 4ac4a1575e67425b5ef4bdca462803eeef17e3b7 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Thu, 24 Sep 2026 05:20:47 +0000 Subject: [PATCH] myna newsletter: crawlproof tracking, A/B variants, stats (on top of #89) Ports the tracked newsletter onto #89's issue model instead of beside it: one `myna newsletter`, one core/newsletter.ts. - track set|status|off: crawlproof tracking id (24 hex) in settings, the secret in the vault. Set, every issue's links go through signed click URLs, the HTML carries an open pixel, and the unsubscribe link is crawlproof's signed one (one-click) instead of myna cloud's. - A/B variants: an issue's subjects (--subject-b) crossed with its CTA set (--cta-set, default "default": Book a demo, Schedule a call, See our plans, Support us), one per person by sha256(issue:email). {{cta}} marks the button. `cta list|add|rm` edits the sets. - --service puts "You get this because you have an account at ; our Terms say we may email news and updates." in the footer. - send: --to (alias of --test) sends one copy as variant A; a list send needs --yes, else prints the count and variant split; --max-per-day and --pace-ms (newsletter.paceMs, 1000); the ledger records msgId and variant per recipient. - Before a send, unsubscribes are pulled from every configured source; a tracked send refuses if crawlproof cannot be read. sync-optouts (alias of sync) pulls both; the daemon does too. - stats : per variant sent, human and all opens, human clicks, CTR, unsubscribes, and the leader, joined through the ledger's msgIds. - SMTP: a part with a line over 998 octets is sent quoted-printable. Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 45 +- apps/cli/src/cli/newsletter.ts | 266 ++++++++++- apps/cli/src/main.ts | 17 +- packages/core/src/core/daemon.ts | 9 +- packages/core/src/core/newsletter.ts | 451 ++++++++++++++++-- packages/core/src/core/smtp.ts | 53 +- packages/core/src/index.ts | 27 +- packages/core/src/store/newsletters.ts | 29 ++ packages/core/src/store/settings.ts | 44 +- .../core/test/newsletter-tracking.test.ts | 364 ++++++++++++++ 10 files changed, 1227 insertions(+), 78 deletions(-) create mode 100644 packages/core/test/newsletter-tracking.test.ts diff --git a/README.md b/README.md index 8763bf6..caf6a98 100644 --- a/README.md +++ b/README.md @@ -869,9 +869,9 @@ myna cloud login # mynaposter.com hosts the un myna newsletter import subscribers.csv --list moshcode # email,name,tags (or .json) myna newsletter subscribe ada@example.com --list moshcode --name Ada myna newsletter create --subject "Moshcode weekly #1" --list moshcode < issue.md -myna newsletter send moshcode-weekly-1 --test you@example.com # one [test] copy +myna newsletter send moshcode-weekly-1 --to you@example.com # one [test] copy (--test works too) myna newsletter send moshcode-weekly-1 --dry-run -myna newsletter send moshcode-weekly-1 +myna newsletter send moshcode-weekly-1 --yes # without --yes: the numbers, nothing sent myna newsletter edit moshcode-weekly-2 --at "friday 9am" # the daemon sends it myna newsletter list | show [--body] | rm [--force] myna newsletter subscribers --list moshcode @@ -900,7 +900,46 @@ it. A refusal is `failed` and is retried only with `--retry-failed`; a send that died mid-message leaves `pending`, retried only with `--retry-uncertain`, because that person may already have it. An issue sent by hand that stopped part way (say with `--limit 5`) waits for a hand; only scheduled issues resume -on their own. +on their own. Messages go out one a second (`newsletter.paceMs`), and +`--max-per-day N` raises the cap for one run. + +### Tracking and A/B tests + +With [crawlproof.com](https://crawlproof.com) tracking set up, every link in an +issue goes through a signed click URL, the HTML part carries an open pixel, +and the unsubscribe link is crawlproof's signed one (one-click too) instead of +myna cloud's, so no `myna cloud login` is needed. + +```bash +myna newsletter track set --secret # from the project's Tracking tab; the secret goes in the vault +myna newsletter track status --check +myna newsletter create --id moshcode-001 --list moshcode-users --service moshcode \ + --subject "What shipped in moshcode" --subject-b "Five new things in moshcode" < issue.md +myna newsletter send moshcode-001 --dry-run # the variants and how the list splits +myna newsletter stats moshcode-001 # per variant: sent, opens, clicks, CTR, unsubscribes +myna newsletter sync-optouts # unsubscribes from myna cloud and crawlproof +myna newsletter cta list | add "