Hypertext Rails
Documentation
Getting Started
Communication Center
- Automation Workflow Flow
- Trigger Events and Cadence Rules
- Fallback Channel Implementation
- Fallback Channel Testing (dev)
- Twilio SMS Integration Guide
- Email Tracking Setup (sent, delivered, failed, open, click)
- SMS Tracking & Twilio Free Tier
- AWS SES Delivery Tracking (console setup for delivery webhook)
- Compiled Template Guide (layout, components, variables)
- Workflow & Template Features (project-driven recipients, multi-project format)
Procore / Project Groups
- Procore Integration — Complete Guide (installation single/grouped, project groups, token storage, why no migration)
Other Features
- Heartbeats Dashboard (kiosk connectivity, queries, sample data)
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/ses → SesController 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=TOKEN → TrackingController#open → CommsEvent opened |
| Click | Recipient clicks a link; we redirect and record | GET /track/click?t=TOKEN&u=... → TrackingController#click → CommsEvent 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 viabefore_create :set_tracking_token). - Used in URLs: All tracking URLs use
t=TOKEN. The legacyd=DELIVERY_IDformat 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
srcis 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 byt(token), creates oneopenedevent 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/httpslink (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 fromApplicationMailer#delivery_email. Only wraps links when delivery has a tracking token. - Controller:
TrackingController#click— finds delivery byt(token), decodesu, creates aclickedevent, then redirects to the decoded URL (onlyhttp/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 successfuldeliver_now/ SMTP handoff). - What: Delivery
status→'sent', and aCommsEventwithevent_type: 'sent'. Optionally we store the provider’s message id inexternal_message_idif 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::SesControllerparses the event, finds the delivery (byX-Peptalk-Delivery-IDin the original headers, or byexternal_message_id), setsstatus: 'delivered', and creates aCommsEventwithevent_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,
CommsDeliverySendJobsetsstatus: 'failed', storesfailure_reason(and anyerror_code/error_message), and creates aCommsEventwithevent_type: 'failed'. - Bounce / Complaint (SES): When SES sends a bounce or complaint notification to the same
POST /webhooks/sesendpoint, the controller finds the delivery the same way and creates afailedevent 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. inconfig/environments/production.rb). Sethost(and optionallyprotocol) so open/click URLs point at your public app (e.g.https://app.peptalk.com). You can useENV['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_SETin 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.