v0.15.1
12 October 2026 Latest- Decode HTML-escaped equals signs as well as ampersands before wrapping tracked links, so password-reset and other query-string URLs retain their parameters.
Every release of @huloglobal/vendure-plugin-email-tracking. Latest release: v0.15.1 — 12 October 2026.
SMTP_SERVER/SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD/SMTP_PASS and SMTP_FROM environment (the plugin has no mailer of its own); without SMTP nothing is sent and nothing breaks. The recipient defaults to the licence's customer email, else the address given for the trial reminder. Admin endpoints under /email-track/digest/: GET settings, POST settings ({enabled?, recipient?}), POST send-test ({to?}) and GET preview (HTML). The Email Log page has a "Weekly report" card (toggle, recipient, last/next send, Send me a test, Preview).GET /email-track/setup: sender wrapped (a log row in the last 30 days), publicBaseUrl set to a public host, bounce webhook secret configured, first open or click recorded. The undone items become the weekly report's tips. "Hide" (per browser) once everything is done; the card returns if an item comes undone.GET /email-track/widget.emailsTracked, emailOpens, emailClicks, emailsBounced and openRatePct for the last 30 days (numbers only, no addresses) go with the evaluation ping and, from this version, with the daily heartbeat too, so renewal mail can say what the plugin did. HULO_HEARTBEAT_DISABLED=true still stops the heartbeat.GET /email-track/licence/status now carries notice (trial ending / key expiring / evaluation ended), premiumFeatures and recentStats (top three non-zero 30-day figures). The licensed banner shows "renews automatically — current key valid until <date>" for subscriptions and the notice in an amber strip; the trial banner has "Email me before it ends" (POST /email-track/eval/remind-me {email}, forwarded to the licence server and kept as the report's default recipient), the premium feature list and a "So far: …" line; the free banner shows the same with "During your evaluation: …" once the evaluation has run.premium-feature, licence-required or licence_required) renders one message in the admin: "<what you tried> needs a licence — start the free trial or activate a key at the top of this page."signingSecret a random per-install secret is generated once at boot and stored in hulo_licence_store (key vendure-plugin-email-tracking:signing-secret; first writer wins so server and worker sign and verify alike), then applied as if configured. Bare-id links in already-sent emails behave exactly as in 0.14.2 (pixel served, clicks to own hosts only). A boot warning explains that setting signingSecret keeps links valid across reinstalls. A generated secret is never used to authenticate the bounce webhook./email-track/click/:token accepts the 64-hex tokens minted by EmailLinkService.issueLinkToken() (?u=…&s=…): the row must exist and be unexpired, and both the HMAC and the stored destination hash must match, so the token binds its URL. Sensitive links are recorded by their redacted form. EmailLinkService signs with the env override, else the plugin's signingSecret.src/pg-corpus.test.ts, runs when HULO_PG_URL is set): every raw SQL statement in src/ is translated by the dialect adapter and executed against PostgreSQL 17 with TypeORM-quoted stand-ins.TrackingEmailSender delegated to NodemailerEmailSender, which discards nodemailer's result, so rows for order confirmations, OTPs and password resets never had a smtpMessageId/smtpResponse. A CapturingSender subclass keeps the result per send; the row now gets the Message-ID (stored without angle brackets, also for sendTracked) and the SMTP reply, and its status is set from the reply code like sendTracked does.email_log in batches of 5 000 (oldest id first, short pause between batches) instead of one unbounded statement, applies maxRows the same way, and prunes email_link rows past expiresAt or older than days.dist.datetime, which TypeORM rejects on Postgres at start-up; they are now portable Date columns. Raw SQL quotes every camelCase column and uses SUM(CASE …) instead of SUM(boolean), so the log, summaries, exports and the GraphQL list work on Postgres.huloEmailLogs selected a lastClickedAt column that does not exist and failed on every database.publicBaseUrl, trackedHosts, clickRedirectAllowedDomains); signed links are unchanged. Bare ids must be plain decimals (a 64-hex link token no longer resolves to row 7).allowUnauthenticatedBounceWebhook: true; it is rate limited; Message-IDs match with or without angle brackets; soft/transient bounces (bounceType: 'soft') are recorded as deferred and never suppress the address; suppression inserts are race-safe.UPDATE … + 1 statements (concurrent pixel fetches from Gmail's proxy and Apple MPP used to lose increments); the pixel is no longer display:none (Outlook skips hidden images) and no longer carries a same-origin resource policy.href contains & are decoded before being wrapped, so click-through query strings survive.=, +, -, @ are quoted so they cannot run as spreadsheet formulas; suppression reasons are checked against the known set.@, the restart poll is cancelled on destroy.retention defaults to { days: 365 } (was: keep everything).trustedIpHeaders (proxy headers that may override req.ip; default none) and allowUnauthenticatedBounceWebhook.id DESC (same order, uses the primary key).hulo-global-logo.svg is now the HG wordmark on the HULO black rounded square (the same mark as the huloglobal.com header), with explicit -light and -dark variants alongside the auto-switching default..env edit, no restart. Renewed subscription keys are picked up automatically too. New admin endpoints licence/purchase-link and licence/claim-status.@huloglobal/vendure-licence-sdk ^0.12.0.dbConnectionOptions use. Verified against PostgreSQL 17. MySQL/MariaDB installs are unaffected (byte-identical passthrough).customerId lookup in TrackingEmailSender. If the subject doesn't contain an order code (password reset, OTP, email verification, account welcome, etc.) the sender now does a case-insensitive Customer.emailAddress lookup so the row still links to the right customer. Combined with 0.8.1's order-code extraction, this means the per-customer Emails view now surfaces every send the customer ever received — order-related and non-order-related.TrackingEmailSender now populates orderCode, orderId and customerId on every EmailLog row it creates. Before this fix, only emails sent by our own service code (which passed those ids in explicitly) had them set — the Vendure email-plugin's built-in order-confirmation, invoice, password-reset etc. handlers do not hand order/customer entities through to the sender, so those rows saved with all three ids as NULL. That broke the per-order and per-customer Emails buttons on the admin (they filter by orderCode / customerId), showing an empty list even though the emails were sent. The sender now extracts the order code from the email subject via a #<code> regex (Vendure's default order-related templates render it there — e.g. "Order confirmation for #S2BZ54TEK91HUUBA"), then looks up the corresponding Order row to backfill orderId and customerId. Best-effort: unmatched subjects fall back to the previous behaviour (envelope-only row).warnIfIncompatibleVendure(). Logs a non-fatal warning when the runtime @vendure/core version is outside the tested range. Silent when inside; fail-open on unparseable versions.@vendure/core tightened to >=3.5.0 <4.0.0 — Vendure 3.5, 3.6 and 3.7 are all covered. Anything under 3.5 has never been tested by us; anything from 4.0 upwards is deferred until we've seen the changelog.@huloglobal/vendure-licence-sdk@^0.6.0.EmailLink entity — per-link metadata for tracked transactional emails. Each clickable link in each email gets its own random 32-byte token. Records the link's type, human label, visible text, position index, template section (main_cta, footer, etc.), destination host, path, and a SHA-256 hash of the full destination URL.EmailLinkService — issues per-link tokens, verifies HMAC signatures on click, looks up link metadata at redirect time. Graceful degradation: if the host doesn't have the migration yet, the redirect still works via signature verification alone; the click event just won't carry server-side link metadata.isSensitive: true on password-reset, invoice-access, or licence-delivery URLs. The raw destination never lands on an admin-visible event row; only the redacted form and hash.@huloglobal/vendure-licence-sdk@^0.5.0 for the shared classifyEmailEvent() classifier.huloEmailLogs, huloEmailLog, huloEmailStatsByTemplate (paid), huloEmailSuppressions, huloAddEmailSuppression, huloRemoveEmailSuppression.npm view --json <pkg> dist.attestations.display: block on mobile tables that broke the row / cell alignment. Table now scrolls horizontally inside its card at narrow widths.signingSecret is configured, the URLs embed an HMAC tag and forged ids are rejected.clickRedirectAllowedDomains).POST /email-track/bounce webhook (bounceWebhookSecret).hashIpsInHistory, default true; ipSalt setting)./open + /click.options.retention.@huloglobal/vendure-licence-sdk@^0.2.0.UpdateChecker integration via the licence-sdk — /email-track/status endpoint returns version + update info, admin UI shows a banner when a new version is available.EmailSuppression entity. Hard bounces and complaints auto-add to the table; sendTracked() refuses recipients on the list and writes status='suppressed'. CRUD endpoints (GET /email-track/suppression, POST /email-track/suppression, DELETE /email-track/suppression/:recipient).GET /email-track/log/stats/by-template returns open rate, click rate and click-to-open per email type.GET /email-track/log/export.csv mirrors the list endpoint's filters (max 50 000 rows).opensJson) alongside the existing click history. Capped to the last 50 opens per email — older opens still contribute to openCount. Surfaced as opens: [] on GET /email-track/log/:id.EmailTrackingPlugin — wraps @vendure/email-plugin and persists every send to the email_log table.TrackingEmailSender — drop-in EmailSender replacement that wraps the default Nodemailer sender and injects an open-tracking pixel and a click redirector into the outgoing HTML.EmailTrackingService — exposed for custom controllers that send transactional email outside the email-plugin pipeline./email-track/open/:id.gif (1×1 pixel), /email-track/click/:id?u=<encoded> (302 redirect), and /email-track/bounce (webhook hook for DSN parsers)./email-track/log (paginated list with filters), /email-track/log/summary and /email-track/log/:id.@huloglobal/vendure-licence-sdk with revocation polling against the HULO licence server.