Overview
Email Tracking logs every transactional email your Vendure server sends. It wraps the @vendure/email-plugin sender so opens and clicks are tracked automatically, plus exposes a service for ad-hoc sends from your own plugin code.
Data captured per email:
- Send: recipient, subject, type, related order / customer / invoice, the SMTP response, the SMTP
messageId. - Opens: per-open history (last 50) with timestamp, IP, user-agent and parsed client (Gmail web, Outlook desktop, Apple Mail iOS …).
- Clicks: per-click history (last 50) with timestamp, target URL, IP, user-agent.
- Bounces / complaints: status update + auto-add to the suppression list.
Install
One package, one line of config, one migration. The fastest way is the bundled installer:
Or by hand:
Restart Vendure. The Email Log tab appears in the admin nav immediately.
Admin UI tour
The Email Log page is mounted at /admin/extensions/email-log (or via the side nav).
Click Details on any row to expand the timeline. You'll see:
- Header block — From / To / BCC / subject / context
- SMTP block — message-id, SMTP response, any error message
- Open block — first/last open times, first-open IP and UA
- Open history table — every open with time, IP, UA
- Click history table — every click with time, target URL, IP, UA
openCount / clickCount) keep counting beyond 50.Suppression list
The plugin maintains an email_suppression table — recipients who should never be sent to. The table is populated automatically when the bounce webhook reports a hard bounce or a complaint, and you can add entries manually.
When sendTracked() is called for a suppressed recipient, the row is written to email_log with status='suppressed' and errorMessage='Recipient is on the suppression list' — but no SMTP call is made.
Adding entries manually
Lifting a suppression
Listing the table
Bounce webhook
Wire your postmaster integration (or scheduled DSN parser) to POST to /email-track/bounce:
The matching email_log row is updated and the recipient is auto-added to the suppression list. The endpoint is unauthenticated — gate it behind a shared secret header if you expose it to the public internet.
Per-template analytics
For an aggregated view of how each email type performs, hit:
Returns:
CSV export
The list endpoint has a CSV mirror. Same filters, same shape:
Up to 50 000 rows per call.
HTTP endpoints
| Method | Path | Description |
|---|---|---|
| GET | /email-track/open/:id.gif | Public pixel — logs an open then serves a 1×1 GIF |
| GET | /email-track/click/:id?u=<url> | Public click redirector — logs then 302s to the original URL |
| POST | /email-track/bounce | Bounce / complaint webhook |
| GET | /email-track/log | Admin: paginated log with filters |
| GET | /email-track/log/summary | Admin: status totals tile |
| GET | /email-track/log/:id | Admin: full detail (incl. opens + clicks arrays) |
| GET | /email-track/log/stats/by-template | Admin: per-template aggregates |
| GET | /email-track/log/export.csv | Admin: CSV export |
| GET | /email-track/suppression | Admin: list suppressions |
| POST | /email-track/suppression | Admin: add a suppression |
| DELETE | /email-track/suppression/:recipient | Admin: lift a suppression |
Troubleshooting
Opens aren't being recorded
Many email clients prefetch images through a proxy (Gmail's googleimageproxy, Outlook's SafeLinks). The plugin recognises common proxies and flags those opens as isBot: true on the open entry — they still count in openCount but you can filter them out in the UI.
Clicks redirect to a "blank target" 400
The redirector refuses URLs that don't start with http:// or https:// — including javascript:, data:, mailto:. The link rewriter already excludes mailto: and unsubscribe-style links from rewriting, so you shouldn't see this in normal use.
Suppression list isn't catching recent bounces
Make sure your bounce / DSN webhook is hitting /email-track/bounce with a messageId that matches what your SMTP transport returned at send time. Gmail returns the messageId in the 250 2.0.0 OK 1234567890 [email protected] response — the plugin stores it as smtpMessageId on the EmailLog row.