mailery 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { R as RunnerContext, N as NormalizedEvent, C as ContactAdapter, a as Contact, A as AdapterFilter, M as MailProvider, S as SendArgs, b as SendResult, c as Mailer, T as TemplateDoc, d as SuppressionScope, F as FlowStep, e as SegmentFilter, P as Predicate } from './null-DaisDvB_.cjs';
2
- export { f as AuditLogDoc, B as BroadcastDoc, g as BroadcastStatus, h as CircuitBreakerThresholds, i as Collections, j as ContactTagDoc, E as EventDoc, k as FlowDoc, l as FlowGoal, m as FlowRunDoc, n as FlowRunStatus, o as FlowVersionDoc, H as HealthDoc, p as HealthStatus, L as LeadDoc, q as MailerConfig, r as NullProvider, O as OutboxDoc, s as RedisOptions, t as SegmentDefinition, u as SendDoc, v as SendStatus, w as SenderDomainConfig, x as SenderDomainRegistry, y as SenderDomainValidation, z as SubscriptionDoc, D as SubscriptionStatus, G as SuppressionDoc, I as SuppressionReason, J as TemplateKind, K as TemplateVersionDoc, W as WebhookEventDoc, Q as ensureIndexes, U as getCollections, V as validateSenderDomain } from './null-DaisDvB_.cjs';
1
+ import { R as RunnerContext, N as NormalizedEvent, C as ContactAdapter, a as Contact, A as AdapterFilter, M as MailProvider, S as SendArgs, b as SendResult, c as Mailer, T as TemplateDoc, d as SuppressionScope, F as FlowStep, e as SegmentFilter, P as Predicate } from './null-CGaNDDlQ.cjs';
2
+ export { f as AuditLogDoc, B as BroadcastDoc, g as BroadcastStatus, h as CircuitBreakerThresholds, i as Collections, j as ContactTagDoc, E as EventDoc, k as FlowDoc, l as FlowGoal, m as FlowRunDoc, n as FlowRunStatus, o as FlowVersionDoc, H as HealthDoc, p as HealthStatus, L as LeadDoc, q as MailerConfig, r as NullProvider, O as OutboxDoc, s as RedisOptions, t as SegmentDefinition, u as SendDoc, v as SendStatus, w as SenderDomainConfig, x as SenderDomainRegistry, y as SenderDomainValidation, z as SubscriptionDoc, D as SubscriptionStatus, G as SuppressionDoc, I as SuppressionReason, J as TemplateKind, K as TemplateVersionDoc, W as WebhookEventDoc, Q as ensureIndexes, U as getCollections, V as validateSenderDomain } from './null-CGaNDDlQ.cjs';
3
3
  import { ObjectId, Db, Filter } from 'mongodb';
4
4
  import { Request, Router } from 'express';
5
5
  import Handlebars from 'handlebars';
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { R as RunnerContext, N as NormalizedEvent, C as ContactAdapter, a as Contact, A as AdapterFilter, M as MailProvider, S as SendArgs, b as SendResult, c as Mailer, T as TemplateDoc, d as SuppressionScope, F as FlowStep, e as SegmentFilter, P as Predicate } from './null-DaisDvB_.js';
2
- export { f as AuditLogDoc, B as BroadcastDoc, g as BroadcastStatus, h as CircuitBreakerThresholds, i as Collections, j as ContactTagDoc, E as EventDoc, k as FlowDoc, l as FlowGoal, m as FlowRunDoc, n as FlowRunStatus, o as FlowVersionDoc, H as HealthDoc, p as HealthStatus, L as LeadDoc, q as MailerConfig, r as NullProvider, O as OutboxDoc, s as RedisOptions, t as SegmentDefinition, u as SendDoc, v as SendStatus, w as SenderDomainConfig, x as SenderDomainRegistry, y as SenderDomainValidation, z as SubscriptionDoc, D as SubscriptionStatus, G as SuppressionDoc, I as SuppressionReason, J as TemplateKind, K as TemplateVersionDoc, W as WebhookEventDoc, Q as ensureIndexes, U as getCollections, V as validateSenderDomain } from './null-DaisDvB_.js';
1
+ import { R as RunnerContext, N as NormalizedEvent, C as ContactAdapter, a as Contact, A as AdapterFilter, M as MailProvider, S as SendArgs, b as SendResult, c as Mailer, T as TemplateDoc, d as SuppressionScope, F as FlowStep, e as SegmentFilter, P as Predicate } from './null-CGaNDDlQ.js';
2
+ export { f as AuditLogDoc, B as BroadcastDoc, g as BroadcastStatus, h as CircuitBreakerThresholds, i as Collections, j as ContactTagDoc, E as EventDoc, k as FlowDoc, l as FlowGoal, m as FlowRunDoc, n as FlowRunStatus, o as FlowVersionDoc, H as HealthDoc, p as HealthStatus, L as LeadDoc, q as MailerConfig, r as NullProvider, O as OutboxDoc, s as RedisOptions, t as SegmentDefinition, u as SendDoc, v as SendStatus, w as SenderDomainConfig, x as SenderDomainRegistry, y as SenderDomainValidation, z as SubscriptionDoc, D as SubscriptionStatus, G as SuppressionDoc, I as SuppressionReason, J as TemplateKind, K as TemplateVersionDoc, W as WebhookEventDoc, Q as ensureIndexes, U as getCollections, V as validateSenderDomain } from './null-CGaNDDlQ.js';
3
3
  import { ObjectId, Db, Filter } from 'mongodb';
4
4
  import { Request, Router } from 'express';
5
5
  import Handlebars from 'handlebars';
package/dist/index.js CHANGED
@@ -817,6 +817,11 @@ function adaptBullQueue(q) {
817
817
  return {
818
818
  add: (name, data, opts) => q.add(name, data, opts),
819
819
  getWaitingCount: () => q.getWaitingCount(),
820
+ getInFlightCount: async () => {
821
+ const [active, waiting] = await Promise.all([q.getActiveCount(), q.getWaitingCount()]);
822
+ return active + waiting;
823
+ },
824
+ getDelayedCount: () => q.getDelayedCount(),
820
825
  close: () => q.close()
821
826
  };
822
827
  }
@@ -3247,43 +3252,121 @@ function createAdminRouter(mailer, opts = {}) {
3247
3252
  function apiRouter(mailer) {
3248
3253
  const r = Router();
3249
3254
  const c = mailer.collections;
3250
- r.get("/me", (req, res) => {
3251
- res.json({
3252
- actor: req.actor,
3253
- permissions: { canPublish: true, canSendBroadcasts: true, canManageSuppressions: true }
3254
- });
3255
- });
3255
+ r.get(
3256
+ "/me",
3257
+ asyncHandler(async (req, res) => {
3258
+ const [flows, templates, broadcasts, contacts, suppressions, health] = await Promise.all([
3259
+ c.flows.estimatedDocumentCount(),
3260
+ c.templates.estimatedDocumentCount(),
3261
+ c.broadcasts.estimatedDocumentCount(),
3262
+ c.subscriptions.countDocuments({ status: "subscribed" }),
3263
+ c.suppressions.estimatedDocumentCount(),
3264
+ c.health.findOne({ _id: "singleton" })
3265
+ ]);
3266
+ res.json({
3267
+ actor: req.actor,
3268
+ permissions: { canPublish: true, canSendBroadcasts: true, canManageSuppressions: true },
3269
+ counts: { flows, templates, broadcasts, contacts, suppressions },
3270
+ health: { status: health?.status ?? "healthy" },
3271
+ providers: {
3272
+ names: Object.keys(mailer.config.providers),
3273
+ default: mailer.config.defaultProvider
3274
+ }
3275
+ });
3276
+ })
3277
+ );
3256
3278
  r.get(
3257
3279
  "/dashboard",
3258
3280
  asyncHandler(async (_req, res) => {
3259
- const since24h = new Date(Date.now() - 24 * 60 * 60 * 1e3);
3260
- const [sentTotal, deliveredCount, bouncedCount, openedCount, clickedCount] = await Promise.all([
3281
+ const now = Date.now();
3282
+ const HOUR = 60 * 60 * 1e3;
3283
+ const since24h = new Date(now - 24 * HOUR);
3284
+ const since48h = new Date(now - 48 * HOUR);
3285
+ const [
3286
+ sentTotal,
3287
+ deliveredCount,
3288
+ bouncedCount,
3289
+ openedCount,
3290
+ clickedCount,
3291
+ sentPrev,
3292
+ deliveredPrev,
3293
+ openedPrev,
3294
+ clickedPrev,
3295
+ hourly
3296
+ ] = await Promise.all([
3261
3297
  c.sends.countDocuments({ queuedAt: { $gt: since24h } }),
3262
3298
  c.sends.countDocuments({ queuedAt: { $gt: since24h }, status: "delivered" }),
3263
3299
  c.sends.countDocuments({ queuedAt: { $gt: since24h }, status: "bounced" }),
3264
3300
  c.sends.countDocuments({ queuedAt: { $gt: since24h }, openedAt: { $ne: null } }),
3265
- c.sends.countDocuments({ queuedAt: { $gt: since24h }, firstClickAt: { $ne: null } })
3301
+ c.sends.countDocuments({ queuedAt: { $gt: since24h }, firstClickAt: { $ne: null } }),
3302
+ c.sends.countDocuments({ queuedAt: { $gt: since48h, $lte: since24h } }),
3303
+ c.sends.countDocuments({ queuedAt: { $gt: since48h, $lte: since24h }, status: "delivered" }),
3304
+ c.sends.countDocuments({ queuedAt: { $gt: since48h, $lte: since24h }, openedAt: { $ne: null } }),
3305
+ c.sends.countDocuments({ queuedAt: { $gt: since48h, $lte: since24h }, firstClickAt: { $ne: null } }),
3306
+ c.sends.aggregate([
3307
+ { $match: { queuedAt: { $gt: since24h } } },
3308
+ {
3309
+ $project: {
3310
+ hour: {
3311
+ $toInt: {
3312
+ $divide: [{ $subtract: [now, { $toLong: "$queuedAt" }] }, HOUR]
3313
+ }
3314
+ },
3315
+ opened: { $cond: [{ $ifNull: ["$openedAt", false] }, 1, 0] }
3316
+ }
3317
+ },
3318
+ { $group: { _id: "$hour", sends: { $sum: 1 }, opens: { $sum: "$opened" } } }
3319
+ ]).toArray()
3266
3320
  ]);
3321
+ const sendSeries = new Array(24).fill(0);
3322
+ const openSeries = new Array(24).fill(0);
3323
+ for (const row of hourly) {
3324
+ const idx = 23 - Math.max(0, Math.min(23, row._id));
3325
+ sendSeries[idx] = row.sends;
3326
+ openSeries[idx] = row.opens;
3327
+ }
3328
+ const delta = (cur, prev) => {
3329
+ if (prev === 0) return null;
3330
+ return (cur - prev) / prev;
3331
+ };
3332
+ const rateDelta = (curN, curD, prevN, prevD) => {
3333
+ if (prevD === 0 || curD === 0) return null;
3334
+ return curN / curD - prevN / prevD;
3335
+ };
3267
3336
  const health = await c.health.findOne({ _id: "singleton" });
3268
3337
  const recentFlows = await c.flows.find({ enabled: true }).limit(5).toArray();
3269
3338
  const recentSends = await c.sends.find().sort({ queuedAt: -1 }).limit(6).toArray();
3270
3339
  const recentAudit = await c.auditLog.find().sort({ occurredAt: -1 }).limit(5).toArray();
3340
+ const queueCounts = await collectQueueCounts(mailer);
3341
+ const lastSendError = await c.sends.findOne({ status: { $in: ["bounced", "failed"] } }, { sort: { queuedAt: -1 } });
3342
+ const lastSendOk = await c.sends.findOne({ status: "delivered" }, { sort: { queuedAt: -1 } });
3343
+ const providerOk = lastSendOk && lastSendError ? new Date(lastSendOk.queuedAt).getTime() >= new Date(lastSendError.queuedAt).getTime() : lastSendOk ? true : lastSendError ? false : null;
3271
3344
  res.json({
3272
3345
  kpis: {
3273
- sends: { value: sentTotal, delta: null },
3346
+ sends: { value: sentTotal, delta: delta(sentTotal, sentPrev) },
3274
3347
  deliveredRate: {
3275
- value: sentTotal === 0 ? 1 : deliveredCount / sentTotal,
3276
- delta: null,
3348
+ value: sentTotal === 0 ? null : deliveredCount / sentTotal,
3349
+ delta: rateDelta(deliveredCount, sentTotal, deliveredPrev, sentPrev),
3277
3350
  bounced: bouncedCount
3278
3351
  },
3279
- openRate: { value: sentTotal === 0 ? 0 : openedCount / sentTotal, delta: null, exclBots: false },
3280
- clickRate: { value: sentTotal === 0 ? 0 : clickedCount / sentTotal, delta: null }
3352
+ openRate: {
3353
+ value: sentTotal === 0 ? null : openedCount / sentTotal,
3354
+ delta: rateDelta(openedCount, sentTotal, openedPrev, sentPrev),
3355
+ exclBots: false
3356
+ },
3357
+ clickRate: {
3358
+ value: sentTotal === 0 ? null : clickedCount / sentTotal,
3359
+ delta: rateDelta(clickedCount, sentTotal, clickedPrev, sentPrev)
3360
+ }
3281
3361
  },
3282
- health: health ? { status: health.status, rates: health.rates } : {
3283
- status: "healthy",
3284
- rates: { hardBounceRate: 0, complaintRate: 0, combinedBounceRate: 0, failureRate: 0 }
3362
+ series: { hourly: { sends: sendSeries, opens: openSeries } },
3363
+ health: health ? { status: health.status, rates: health.rates } : { status: "healthy", rates: { hardBounceRate: 0, complaintRate: 0, combinedBounceRate: 0, failureRate: 0 } },
3364
+ queue: {
3365
+ inFlight: queueCounts?.inFlight ?? null,
3366
+ delayed: queueCounts?.delayed ?? null,
3367
+ providerOk,
3368
+ providerName: mailer.config.defaultProvider
3285
3369
  },
3286
- queue: { inFlight: 0, delayed: 0, providerOk: true, providerName: mailer.config.defaultProvider },
3287
3370
  recentFlows,
3288
3371
  recentSends,
3289
3372
  recentAudit
@@ -3460,6 +3543,13 @@ function apiRouter(mailer) {
3460
3543
  );
3461
3544
  })
3462
3545
  );
3546
+ r.get(
3547
+ "/health/trips",
3548
+ asyncHandler(async (_req, res) => {
3549
+ const rows = await c.auditLog.find({ action: { $in: ["health.trip", "health.resume"] } }).sort({ occurredAt: -1 }).limit(50).toArray();
3550
+ res.json(rows);
3551
+ })
3552
+ );
3463
3553
  r.post(
3464
3554
  "/health/resume",
3465
3555
  asyncHandler(async (req, res) => {
@@ -3983,6 +4073,28 @@ function asyncHandler(fn) {
3983
4073
  fn(req, res, next).catch(next);
3984
4074
  };
3985
4075
  }
4076
+ async function collectQueueCounts(mailer) {
4077
+ const qs = Object.values(mailer.queues);
4078
+ const sum = async (key) => {
4079
+ let total = 0;
4080
+ let supported = false;
4081
+ for (const q of qs) {
4082
+ const fn = q[key];
4083
+ if (!fn) continue;
4084
+ try {
4085
+ const v = await fn.call(q);
4086
+ if (v == null) continue;
4087
+ total += v;
4088
+ supported = true;
4089
+ } catch {
4090
+ return null;
4091
+ }
4092
+ }
4093
+ return supported ? total : null;
4094
+ };
4095
+ const [inFlight, delayed] = await Promise.all([sum("getInFlightCount"), sum("getDelayedCount")]);
4096
+ return { inFlight, delayed };
4097
+ }
3986
4098
  var PIXEL = Buffer.from(
3987
4099
  "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
3988
4100
  "base64"