Hypertext Rails

Documentation

Getting Started

Communication Center

Procore / Project Groups

Other Features

Email Tracking Setup

Single reference for email tracking: Sent, Delivered, Failed, Open, and Click.


1. Tracking lifecycle (Sent → Delivered/Failed → Open/Click)

Stage How we track it Where it’s recorded
Sent When the app successfully hands the email to SMTP (e.g. SES) CommsDeliverySendJob sets status: 'sent' and creates CommsEvent with event_type: 'sent'
Delivered AWS SES tells us the message was delivered SES → SNS → POST /webhooks/sesSesController creates CommsEvent delivered and sets delivery status: 'delivered'
Failed Send error, or SES bounce/complaint Job sets status: 'failed' and creates failed event; or SES webhook creates failed event (bounce/complaint)
Open Recipient’s client loads a 1×1 image (tracking pixel) in the email GET /track/open?t=TOKENTrackingController#openCommsEvent opened
Click Recipient clicks a link; we redirect and record GET /track/click?t=TOKEN&u=...TrackingController#clickCommsEvent clicked → redirect to real URL

All events are stored on comms_events (linked to comms_deliveries). Delivery status lives on comms_deliveries.status and external_message_id / external_status when we get them from the provider.


2. Tracking token (for Open and Click)

We use an opaque tracking token per delivery so tracking URLs don’t expose the delivery id.

  • Stored: comms_deliveries.tracking_token (column; set when the delivery is created via before_create :set_tracking_token).
  • Used in URLs: All tracking URLs use t=TOKEN. The legacy d=DELIVERY_ID format is no longer accepted (removed to prevent IDOR).

You don’t need to do anything with the token yourself; the mailer and EmailClickTrackingService use it when building links.


3. Open tracking (pixel)

  • Purpose: Record that the email was “opened” when the recipient’s client loads images.
  • Mechanism: A 1×1 transparent GIF is embedded at the end of the email body. Its src is our tracking URL. When the client loads it, the browser requests that URL; we record an open and return the pixel.
  • URL: GET /track/open?t=TOKEN
  • Where it’s built: app/views/application_mailer/delivery_email.html.erb (only renders the pixel when delivery has a tracking token).
  • Controller: TrackingController#open — finds delivery by t (token), creates one opened event per delivery (findorcreate_by), always returns the same 1×1 GIF.

Limitations: Some clients load images on inbox view; privacy tools can block the pixel. Open counts are best-effort.


4. Click tracking (link wrapping)

  • Purpose: Record that the recipient clicked a link, then send them to the real destination.
  • Mechanism: Before sending, we rewrite every http/https link (and some images) in the HTML body to point to our redirect URL. When the user clicks, we get the request, record a click, then redirect to the original URL.
  • URL: GET /track/click?t=TOKEN&u=BASE64_ENCODED_DESTINATION_URL
  • Where it’s built: EmailClickTrackingService.wrap_links(html, delivery) — called from ApplicationMailer#delivery_email. Only wraps links when delivery has a tracking token.
  • Controller: TrackingController#click — finds delivery by t (token), decodes u, creates a clicked event, then redirects to the decoded URL (only http/https).

Links like mailto:, tel:, and # are not wrapped. The tracking pixel URL is not wrapped.


5. Sent, Delivered, and Failed (provider + job)

Sent

  • When: The mailer sends the email and doesn’t raise.
  • Who: CommsDeliverySendJob (after a successful deliver_now / SMTP handoff).
  • What: Delivery status'sent', and a CommsEvent with event_type: 'sent'. Optionally we store the provider’s message id in external_message_id if we have it.

Delivered (AWS SES)

  • When: SES confirms delivery to the recipient’s mail server.
  • Who: AWS SES publishes to SNS; SNS POSTs to our app at POST /webhooks/ses.
  • What: Webhooks::SesController parses the event, finds the delivery (by X-Peptalk-Delivery-ID in the original headers, or by external_message_id), sets status: 'delivered', and creates a CommsEvent with event_type: 'delivered'.

So “delivered” here means provider-level delivery, not “recipient opened the email” (that’s the open event above).

Failed

  • Send failure: If the job or SMTP fails, CommsDeliverySendJob sets status: 'failed', stores failure_reason (and any error_code / error_message), and creates a CommsEvent with event_type: 'failed'.
  • Bounce / Complaint (SES): When SES sends a bounce or complaint notification to the same POST /webhooks/ses endpoint, the controller finds the delivery the same way and creates a failed event and sets the delivery status to 'failed' (with reason from the payload).

For full SES/SNS setup (topics, event publishing, subscription, “Include original email headers”), see AWSSESDELIVERYTRACKINGSETUP.md.


6. Environment and config

  • Host for tracking URLs: The app uses action_mailer.default_url_options (e.g. in config/environments/production.rb). Set host (and optionally protocol) so open/click URLs point at your public app (e.g. https://app.peptalk.com). You can use ENV['BASE_HOST_URL'] or similar; no mail-specific env var is required for tracking.
  • SES: To get delivered/failed from SES, set up the configuration set and SNS subscription as in the AWS doc, and set SES_CONFIGURATION_SET in the environment so the mailer sends with that configuration set.

7. Quick reference

What Route / source Event type / status
Sent CommsDeliverySendJob sent
Delivered POST /webhooks/ses (SES delivery) delivered
Failed Job or POST /webhooks/ses (bounce/complaint) failed
Open GET /track/open?t=TOKEN opened
Click GET /track/click?t=TOKEN&u=... clicked

Token: stored in comms_deliveries.tracking_token, required for open/click tracking. Legacy d= removed to prevent IDOR.