From 747d398cc2744a77c32fc69336ef06b2d076e652 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Wed, 23 Sep 2026 22:24:36 -0700 Subject: [PATCH 1/4] myna newsletter: crawlproof tracking, A/B variants, stats (port of #90 to main) #90 was merged into feat/newsletter after #89 had already been squashed into main, so its commit (fc102a0) never reached main. This is that commit cherry-picked onto main, with the conflicts against #91 resolved: sync and the daemon report re-subscribes from myna cloud as well as unsubscribes from both sources. Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 45 +- apps/cli/src/cli/newsletter.ts | 268 ++++++++++- 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, 1228 insertions(+), 79 deletions(-) create mode 100644 packages/core/test/newsletter-tracking.test.ts diff --git a/README.md b/README.md index c18c14a..97e422d 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 @@ -905,7 +905,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 "