myna newsletter: crawlproof tracking, A/B variants, stats (stacked on #89) - #90
Merged
Merged
Conversation
ralyodio
marked this pull request as ready for review
September 24, 2026 05:11
) 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 <service>; 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 <id>: 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) <noreply@anthropic.com>
ralyodio
force-pushed
the
feat/newsletter-crawlproof
branch
from
September 24, 2026 05:20
117b65a to
4ac4a15
Compare
ralyodio
marked this pull request as draft
September 24, 2026 05:21
ralyodio
marked this pull request as ready for review
September 24, 2026 05:24
Contributor
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What it adds to #89's
myna newsletterThe code sits in #89's design: one command, one
core/newsletter.ts, the same issues, subscriber lists, per-recipient ledger, myna cloud unsubscribe inbox, daemon job and MCP tools. #89 behaves exactly as before unless tracking,--subject-bor a CTA set is used.crawlproof tracking (
myna newsletter track set <trackingId> [--secret <hex>] | track status [--check] | track off). The 24-hex tracking id goes insettings.newsletter.trackingIdand the secret in the vault. Once it is set, every issue gets:List-Unsubscribe, in place of the myna cloud one. A tracked send does not needmyna cloud login.This follows the contract in Email tracking: per-project tracking URL (opens, clicks, unsubscribes, events API) crawlproof.com#266:
nextis null or an absolute URL to GET, the signature coversuexactly as sent, andmachineis only set on opens and clicks.A/B variants. An issue's subjects (
--subject-bon create/edit) are crossed with its CTA set.createusesdefaultunless--cta-set <name>or--cta-set noneis given. Each person's variant comes fromsha256(issueId:email), so a resumed send never changes it.{{cta}}marks where the button goes; without it the CTA goes last, before the footer.myna newsletter cta list | add | rm [--set]manages the sets. The four defaults are Book a demo, Schedule a call, See our plans and Support us. Changing the variants of an issue that is part way sent is refused.--service moshcodechanges the footer line to "You get this because you have an account at moshcode; our Terms say we may email news and updates." The default line says the person subscribed to the list.send
--to <addr>, an alias of--test, sends one[test]copy as variant A and says so. It is not written to the ledger. An address that has opted out is refused.--yes. Without it, the command prints the counts and the variant split and exits 1.--max-per-day Noverrides the cap for one run.--pace-ms Noverridesnewsletter.paceMs(1000). The daemon paces too.msgId,variant,subjectKeyandctafor each recipient.Unsubscribes. Every send pulls from each source that is set up. For myna cloud this works as in 0.29.0: myna newsletter, with one-click unsubscribe on every issue #89: a failure is a warning. For crawlproof, a failure stops the send before anyone is mailed.
sync-optouts(an alias ofsync) and the daemon pull from both. An address that is not a contact yet is added as opted out.stats <id> [--json]joins events to variants through the ledger's msgIds. Per variant it shows sent, unique human opens (all opens beside them), unique human clicks, CTR and unsubscribes, and names the leader. Machine-flagged clicks are left out as well, so a link scanner cannot pick the winner. Test copies are never counted.SMTP. A MIME part with any line longer than 998 octets is sent quoted-printable. Parts with shorter lines stay 8bit.
Tests
packages/core/test/newsletter-tracking.test.tsadds 11 tests. They run against a fake SMTP server on a local port, with crawlproof played by a stubbed fetch that pages throughnextURLs. No real mail is sent. #89'snewsletter.test.tsis unchanged and passes.bun x tsc --noEmit -p tsconfig.json: cleanbun run test: 549 pass, 19 skip, 0 fail🤖 Generated with Claude Code