Skip to content

Email tracking on every surface: API, CLI, TUI - #268

Merged
ralyodio merged 1 commit into
masterfrom
feat/email-tracking-every-surface
Sep 24, 2026
Merged

ralyodio merged 1 commit into
masterfrom
feat/email-tracking-every-surface

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Closes #267. Anthony: email tracking "needs to support all surfaces, cli/tui/web/pwa/desktop/mobile". Until now it was only reachable on the dashboard's Tracking tab.

Surface Now
API (crp_ bearer) GET /api/v1/email-tracking: every reachable project with role, on/off, tracking id and URLs, and the last day's human opens/clicks/unsubscribes. No secrets. GET /api/v1/email-tracking/<id or hostname>[?secret=1]: the secret only on request, never to a viewer (403). POST .../<enable or disable or rotate>: owners and members only; rotate answers with the new secret.
CLI (in-repo + published @profullstack/crawlproof 0.3.0) crawlproof email-tracking list, show <site> [--secret], enable, disable, rotate, with --json. Piped, show --secret prints only the secret, so crawlproof email-tracking show moshcode.sh --secret | myna newsletter track set <id> works. One implementation in lib/emailTracking/cli.ts; both CLIs hand it their token-authed call.
TUI (crawlproof dashboard) A sixth tab, Email, loaded from the list endpoint. It draws without waiting for the fleet snapshot. e turns the highlighted project on or off.
Web / PWA The existing Tracking tab, unchanged. Desktop and mobile get the PWA via manifest.webmanifest; there are no native apps.

Access follows requireProjectAccess:

  • The project owner, and org owners and members, may change tracking.
  • A project member with role viewer only looks.
  • Projects are found by id, hostname or URL. An ambiguous name gets a 404 asking for the id.

Tests

  • tests/email-tracking-api.test.ts (6): roles, secret gating, ambiguous refs, 401 before any read.
  • tests/email-tracking-surfaces.test.ts (5): the CLI's output, errors and piped secret; the TUI tab rendered through hqtui, the loader and the e toggle.
  • Full vitest: 2460 pass, 9 skipped. tsc --noEmit clean.
  • packages/cli builds, and help lists the new command. Against production it currently 404s cleanly, because the route isn't deployed yet.

After merge and deploy: publish @profullstack/crawlproof 0.3.0. Then myna can add myna newsletter track connect <site>, which reads this API with the crawlproof token myna already holds.

🤖 Generated with Claude Code

Until now a project's email tracking could only be seen or changed on the
dashboard's Tracking tab. Now the same thing is reachable with a crp_
token:

- API: GET /api/v1/email-tracking (every reachable project, role, on/off,
  tracking id, last day's human opens/clicks/unsubscribes, no secrets);
  GET /api/v1/email-tracking/<id|hostname>[?secret=1] (the secret only
  on request and never to a viewer); POST .../<enable|disable|rotate>
  (owners and members only). Roles follow requireProjectAccess.
- CLI, in-repo and the published @profullstack/crawlproof (0.3.0):
  crawlproof email-tracking list | show [--secret] | enable | disable |
  rotate. show --secret piped prints the secret alone, so it pipes into
  myna newsletter track set.
- TUI: a sixth tab, Email, with its own loader; e turns the highlighted
  project on or off.

Closes #267.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

48 finding(s)

HIGH/CRITICAL: 2 | MEDIUM: 31 | LOW: 15

Severity Rule Location
HIGH tls-verification-disabled lib/onion.ts:48
HIGH secret-generic-credential lib/sp/platforms/facebook.ts:32
MEDIUM js-unescaped-html-sink app/(app)/dashboard/admin/email-broadcast/EmailBroadcastForm.tsx:125
MEDIUM js-unescaped-html-sink app/(app)/dashboard/projects/[id]/autoblog/articles/[articleId]/page.tsx:214
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:67
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:97
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:104
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:110
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:186
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:190
MEDIUM js-unescaped-html-sink app/c/[project]/[slug]/page.tsx:77
MEDIUM js-unescaped-html-sink app/c/[project]/page.tsx:57
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:228
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:285
MEDIUM js-unescaped-html-sink app/layout.tsx:129
MEDIUM js-open-redirect app/login/form.tsx:39
MEDIUM js-unescaped-html-sink app/r/[token]/page.tsx:176
MEDIUM js-open-redirect app/signup/form.tsx:43
MEDIUM js-open-redirect components/billing/buy-credits-modal.tsx:98
MEDIUM js-unescaped-html-sink components/json-ld.tsx:8
MEDIUM js-unescaped-html-sink components/report/markdown-view.tsx:15
MEDIUM js-unescaped-html-sink lib/careers/page-templates.ts:198
MEDIUM js-dynamic-code-execution lib/crawl-limits.ts:67
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:41
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:324
MEDIUM redos-nested-quantifier lib/lx/articleGen.ts:99
MEDIUM redos-nested-quantifier lib/tracker/agent-gate.ts:61
MEDIUM sh-remote-script-execution prober/deploy/provision.sh:30
MEDIUM sql-template-interpolation scripts/detect-slot-themes.ts:31
MEDIUM sql-template-interpolation scripts/purge-constructed-keywords.ts:163
MEDIUM sql-template-interpolation scripts/purge-offniche-keywords.ts:124
MEDIUM js-dynamic-code-execution scripts/test-crawl-limits.mjs:14
MEDIUM js-dynamic-code-execution scripts/test-crawl-limits.mjs:24
LOW secret-generic-credential app/(marketing)/docs/autoblog-webhook/page.tsx:145
LOW secret-generic-credential lib/sp/platforms/linkedin.ts:25
LOW js-dynamic-code-execution tests/careers-page-templates.test.ts:21
LOW js-dynamic-code-execution tests/careers-widget-script.test.ts:19
LOW js-dynamic-code-execution tests/careers-widget-script.test.ts:69
LOW js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:51
LOW js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:52
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:20
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:24
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:25
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:26
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:31
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:35
LOW secret-generic-credential tests/contract/posthog-integration.test.ts:13
LOW secret-generic-credential tests/lead-campaign.test.ts:16

Snippets are redacted; ThreatCrush never prints matched credential material.

const c = capture();
expect(await runEmailTracking([], {}, call, c.out)).toBe(0);
expect(c.lines[0]).toMatch(/^on\s+moshcode\.sh\s+e960e0a69972a7f34ea197bb\s+12 opens, 3 clicks, 1 unsubs \(24h\)$/);
expect(c.lines[1]).toMatch(/^off\s+profullstack\.com/);
Comment thread cli/dashboard.ts
state.email.loading = true;
invalidate();
try {
const res = await fetcher(`${base}/api/v1/email-tracking`, { headers });
Comment thread cli/dashboard.ts
const toggle = async (row: EmailTrackingRow): Promise<void> => {
const action = row.enabled ? "disable" : "enable";
try {
const res = await fetcher(`${base}/api/v1/email-tracking/${encodeURIComponent(row.project_id)}/${action}`, { method: "POST", headers });
@ralyodio
ralyodio merged commit 15691ca into master Sep 24, 2026
9 of 10 checks passed
@ralyodio
ralyodio deleted the feat/email-tracking-every-surface branch September 24, 2026 06:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Email tracking on every surface: API, CLI, TUI (not just the dashboard tab)

2 participants