Skip to content

feat(email): add open and click tracking for transactional emails#38

Merged
dviejokfs merged 1 commit intomainfrom
feat/email-tracking
Mar 22, 2026
Merged

feat(email): add open and click tracking for transactional emails#38
dviejokfs merged 1 commit intomainfrom
feat/email-tracking

Conversation

@dviejokfs
Copy link
Contributor

Summary

  • Open tracking: Injects a 1x1 transparent GIF tracking pixel before </body> in HTML emails when track_opens: true is set on the send API. The pixel endpoint (GET /api/emails/{id}/track/open) records the open event with IP and user-agent, increments counters, and always returns the GIF (never leaks email existence).
  • Click tracking: Rewrites all http(s):// links in HTML emails through GET /api/emails/{id}/track/click/{link_index} when track_clicks: true. Records the click event and 302-redirects to the original URL. mailto:, tel:, #anchor, and javascript: links are preserved.
  • Tracking data API: Authenticated endpoints for querying tracking summary (/tracking), event list with filtering (/tracking/events?event_type=open), and per-link click stats (/tracking/links).
  • Plugin system: Added configure_public_routes() to TempsPlugin trait for unauthenticated endpoints.
  • Frontend: Open/click count columns in Sent Emails table, tracking stats card on Email Detail page.

Changes

Area Files What
Migration m20260320_000001_add_email_tracking.rs email_events, email_links tables + tracking columns on emails
Entities email_events.rs, email_links.rs, emails.rs New entities + tracking fields
Service tracking_service.rs HTML transformer, event recording, counter management
Handlers tracking.rs Public (pixel, redirect) + authenticated (summary, events, links)
Plugin plugin.rs, temps-core/plugin.rs Public route support, tracking service wiring
Frontend EmailsSentList.tsx, EmailDetail.tsx, types.gen.ts Tracking UI

Test plan

  • 116 tests passing (90 existing + 26 new)
  • 10 unit tests: HTML transformation, pixel injection, link rewriting, mailto/anchor preservation
  • 12 service integration tests: open/click recording, counter increments, disabled tracking skip, nonexistent email, multi-click
  • 14 HTTP handler tests (tower::oneshot): GIF response, 302 redirect, cache headers, auth endpoints, invalid inputs
  • 1 full E2E flow: open pixel → click redirect → query tracking summary → query events → verify DB state
  • Full workspace cargo check --lib passes
  • All pre-commit hooks pass (fmt, clippy, typos, changelog)

- Inject 1x1 tracking pixel for open tracking
- Rewrite links through click tracking endpoint with 302 redirect
- New email_events and email_links tables with cascade FK
- TrackingService: HTML transformation, event recording, counters
- Public endpoints: GET /emails/{id}/track/open (GIF pixel),
  GET /emails/{id}/track/click/{index} (redirect)
- Authenticated endpoints: tracking summary, events list, links list
- Plugin trait: configure_public_routes() for unauthenticated endpoints
- Frontend: open/click columns in email list, tracking card in detail
- 116 tests including full E2E flow (open → click → query → verify DB)
@dviejokfs dviejokfs merged commit 2e52d9d into main Mar 22, 2026
9 checks passed
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.

1 participant