mailery 0.3.1 → 0.4.0
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/admin/spa/index-CyOYk9BX.js +41 -0
- package/dist/admin/spa/index-CyOYk9BX.js.map +1 -0
- package/dist/admin/spa/index.html +1 -1
- package/dist/admin/spa/template-editor-DjkGIZF_.js +502 -0
- package/dist/admin/spa/{template-editor-BuYSO1FP.js.map → template-editor-DjkGIZF_.js.map} +1 -1
- package/dist/index.cjs +304 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +304 -39
- package/dist/index.js.map +1 -1
- package/dist/{null-DaisDvB_.d.cts → null-CGaNDDlQ.d.cts} +4 -0
- package/dist/{null-DaisDvB_.d.ts → null-CGaNDDlQ.d.ts} +4 -0
- package/dist/testing.cjs +5 -0
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +5 -0
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
- package/dist/admin/spa/index-csgOs39s.js +0 -41
- package/dist/admin/spa/index-csgOs39s.js.map +0 -1
- package/dist/admin/spa/template-editor-BuYSO1FP.js +0 -502
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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,131 @@ function createAdminRouter(mailer, opts = {}) {
|
|
|
3247
3252
|
function apiRouter(mailer) {
|
|
3248
3253
|
const r = Router();
|
|
3249
3254
|
const c = mailer.collections;
|
|
3250
|
-
r.get(
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
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
|
+
counts: { flows, templates, broadcasts, contacts, suppressions },
|
|
3269
|
+
health: { status: health?.status ?? null },
|
|
3270
|
+
providers: {
|
|
3271
|
+
names: Object.keys(mailer.config.providers),
|
|
3272
|
+
default: mailer.config.defaultProvider
|
|
3273
|
+
},
|
|
3274
|
+
broadcastConfirmationThreshold: mailer.config.broadcastConfirmationThreshold
|
|
3275
|
+
});
|
|
3276
|
+
})
|
|
3277
|
+
);
|
|
3256
3278
|
r.get(
|
|
3257
3279
|
"/dashboard",
|
|
3258
3280
|
asyncHandler(async (_req, res) => {
|
|
3259
|
-
const
|
|
3260
|
-
const
|
|
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
|
-
const
|
|
3337
|
+
const recentFlowsRaw = await c.flows.find({ enabled: true }).limit(5).toArray();
|
|
3338
|
+
const flowStatsMap = await computeFlowStats(mailer);
|
|
3339
|
+
const recentFlows = recentFlowsRaw.map((f) => ({ ...f, stats: flowStatsMap.get(f.slug) ?? emptyFlowStats() }));
|
|
3269
3340
|
const recentSends = await c.sends.find().sort({ queuedAt: -1 }).limit(6).toArray();
|
|
3270
3341
|
const recentAudit = await c.auditLog.find().sort({ occurredAt: -1 }).limit(5).toArray();
|
|
3342
|
+
const queueCounts = await collectQueueCounts(mailer);
|
|
3343
|
+
const lastSendError = await c.sends.findOne({ status: { $in: ["bounced", "failed"] } }, { sort: { queuedAt: -1 } });
|
|
3344
|
+
const lastSendOk = await c.sends.findOne({ status: "delivered" }, { sort: { queuedAt: -1 } });
|
|
3345
|
+
const providerOk = lastSendOk && lastSendError ? new Date(lastSendOk.queuedAt).getTime() >= new Date(lastSendError.queuedAt).getTime() : lastSendOk ? true : lastSendError ? false : null;
|
|
3271
3346
|
res.json({
|
|
3272
3347
|
kpis: {
|
|
3273
|
-
sends: { value: sentTotal, delta:
|
|
3348
|
+
sends: { value: sentTotal, delta: delta(sentTotal, sentPrev) },
|
|
3274
3349
|
deliveredRate: {
|
|
3275
|
-
value: sentTotal === 0 ?
|
|
3276
|
-
delta:
|
|
3350
|
+
value: sentTotal === 0 ? null : deliveredCount / sentTotal,
|
|
3351
|
+
delta: rateDelta(deliveredCount, sentTotal, deliveredPrev, sentPrev),
|
|
3277
3352
|
bounced: bouncedCount
|
|
3278
3353
|
},
|
|
3279
|
-
openRate: {
|
|
3280
|
-
|
|
3354
|
+
openRate: {
|
|
3355
|
+
value: sentTotal === 0 ? null : openedCount / sentTotal,
|
|
3356
|
+
delta: rateDelta(openedCount, sentTotal, openedPrev, sentPrev)
|
|
3357
|
+
},
|
|
3358
|
+
clickRate: {
|
|
3359
|
+
value: sentTotal === 0 ? null : clickedCount / sentTotal,
|
|
3360
|
+
delta: rateDelta(clickedCount, sentTotal, clickedPrev, sentPrev)
|
|
3361
|
+
}
|
|
3362
|
+
},
|
|
3363
|
+
series: { hourly: { sends: sendSeries, opens: openSeries } },
|
|
3364
|
+
health: {
|
|
3365
|
+
status: health?.status ?? null,
|
|
3366
|
+
rates: health?.rates ?? null,
|
|
3367
|
+
thresholds: {
|
|
3368
|
+
hardBounceRatePctTrip: mailer.config.circuitBreaker.hardBounceRatePctTrip,
|
|
3369
|
+
complaintRatePctTrip: mailer.config.circuitBreaker.complaintRatePctTrip,
|
|
3370
|
+
combinedBounceRatePctTrip: mailer.config.circuitBreaker.combinedBounceRatePctTrip,
|
|
3371
|
+
failedToSendRatePctDegrade: mailer.config.circuitBreaker.failedToSendRatePctDegrade
|
|
3372
|
+
}
|
|
3281
3373
|
},
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3374
|
+
queue: {
|
|
3375
|
+
inFlight: queueCounts?.inFlight ?? null,
|
|
3376
|
+
delayed: queueCounts?.delayed ?? null,
|
|
3377
|
+
providerOk,
|
|
3378
|
+
providerName: mailer.config.defaultProvider
|
|
3285
3379
|
},
|
|
3286
|
-
queue: { inFlight: 0, delayed: 0, providerOk: true, providerName: mailer.config.defaultProvider },
|
|
3287
3380
|
recentFlows,
|
|
3288
3381
|
recentSends,
|
|
3289
3382
|
recentAudit
|
|
@@ -3294,7 +3387,8 @@ function apiRouter(mailer) {
|
|
|
3294
3387
|
"/flows",
|
|
3295
3388
|
asyncHandler(async (_req, res) => {
|
|
3296
3389
|
const flows = await c.flows.find().sort({ updatedAt: -1 }).toArray();
|
|
3297
|
-
|
|
3390
|
+
const stats = await computeFlowStats(mailer);
|
|
3391
|
+
res.json(flows.map((f) => ({ ...f, stats: stats.get(f.slug) ?? emptyFlowStats() })));
|
|
3298
3392
|
})
|
|
3299
3393
|
);
|
|
3300
3394
|
r.get(
|
|
@@ -3302,7 +3396,8 @@ function apiRouter(mailer) {
|
|
|
3302
3396
|
asyncHandler(async (req, res) => {
|
|
3303
3397
|
const flow = await c.flows.findOne({ slug: req.params.slug });
|
|
3304
3398
|
if (!flow) return res.status(404).json({ error: "not_found" });
|
|
3305
|
-
|
|
3399
|
+
const stats = (await computeFlowStats(mailer, flow.slug)).get(flow.slug) ?? emptyFlowStats();
|
|
3400
|
+
return res.json({ ...flow, stats });
|
|
3306
3401
|
})
|
|
3307
3402
|
);
|
|
3308
3403
|
r.post(
|
|
@@ -3337,7 +3432,8 @@ function apiRouter(mailer) {
|
|
|
3337
3432
|
"/templates",
|
|
3338
3433
|
asyncHandler(async (_req, res) => {
|
|
3339
3434
|
const templates = await c.templates.find().sort({ updatedAt: -1 }).toArray();
|
|
3340
|
-
|
|
3435
|
+
const stats = await computeTemplateStats(mailer);
|
|
3436
|
+
res.json(templates.map((t) => ({ ...t, stats: stats.get(t.slug) ?? emptyTemplateStats() })));
|
|
3341
3437
|
})
|
|
3342
3438
|
);
|
|
3343
3439
|
r.get(
|
|
@@ -3345,14 +3441,16 @@ function apiRouter(mailer) {
|
|
|
3345
3441
|
asyncHandler(async (req, res) => {
|
|
3346
3442
|
const template = await c.templates.findOne({ slug: req.params.slug });
|
|
3347
3443
|
if (!template) return res.status(404).json({ error: "not_found" });
|
|
3348
|
-
|
|
3444
|
+
const stats = (await computeTemplateStats(mailer, template.slug)).get(template.slug) ?? emptyTemplateStats();
|
|
3445
|
+
return res.json({ ...template, stats });
|
|
3349
3446
|
})
|
|
3350
3447
|
);
|
|
3351
3448
|
r.get(
|
|
3352
3449
|
"/broadcasts",
|
|
3353
3450
|
asyncHandler(async (_req, res) => {
|
|
3354
3451
|
const broadcasts = await c.broadcasts.find().sort({ createdAt: -1 }).toArray();
|
|
3355
|
-
|
|
3452
|
+
const stats = await computeBroadcastStats(mailer);
|
|
3453
|
+
res.json(broadcasts.map((b) => ({ ...b, stats: stats.get(String(b._id)) ?? emptyBroadcastStats() })));
|
|
3356
3454
|
})
|
|
3357
3455
|
);
|
|
3358
3456
|
r.get(
|
|
@@ -3360,7 +3458,8 @@ function apiRouter(mailer) {
|
|
|
3360
3458
|
asyncHandler(async (req, res) => {
|
|
3361
3459
|
const broadcast = await c.broadcasts.findOne({ slug: req.params.slug });
|
|
3362
3460
|
if (!broadcast) return res.status(404).json({ error: "not_found" });
|
|
3363
|
-
|
|
3461
|
+
const stats = (await computeBroadcastStats(mailer, broadcast._id)).get(String(broadcast._id)) ?? emptyBroadcastStats();
|
|
3462
|
+
return res.json({ ...broadcast, stats });
|
|
3364
3463
|
})
|
|
3365
3464
|
);
|
|
3366
3465
|
r.get(
|
|
@@ -3368,8 +3467,20 @@ function apiRouter(mailer) {
|
|
|
3368
3467
|
asyncHandler(async (req, res) => {
|
|
3369
3468
|
const cursor = typeof req.query.cursor === "string" ? req.query.cursor : void 0;
|
|
3370
3469
|
const limit = Math.min(Number(req.query.limit ?? 50), 200);
|
|
3371
|
-
const { contacts, nextCursor } = await
|
|
3372
|
-
|
|
3470
|
+
const [{ contacts, nextCursor }, counts] = await Promise.all([
|
|
3471
|
+
mailer.adapter.query({}, { limit, cursor }),
|
|
3472
|
+
(async () => {
|
|
3473
|
+
const rows = await c.subscriptions.aggregate([{ $group: { _id: "$status", n: { $sum: 1 } } }]).toArray();
|
|
3474
|
+
const out = { subscribed: 0, pending_doi: 0, unsubscribed: 0, bounced: 0, complained: 0 };
|
|
3475
|
+
let total = 0;
|
|
3476
|
+
for (const r2 of rows) {
|
|
3477
|
+
if (r2._id) out[r2._id] = r2.n;
|
|
3478
|
+
total += r2.n;
|
|
3479
|
+
}
|
|
3480
|
+
return { ...out, total };
|
|
3481
|
+
})()
|
|
3482
|
+
]);
|
|
3483
|
+
res.json({ contacts, nextCursor, counts });
|
|
3373
3484
|
})
|
|
3374
3485
|
);
|
|
3375
3486
|
r.get(
|
|
@@ -3448,16 +3559,25 @@ function apiRouter(mailer) {
|
|
|
3448
3559
|
"/health",
|
|
3449
3560
|
asyncHandler(async (_req, res) => {
|
|
3450
3561
|
const h = await c.health.findOne({ _id: "singleton" });
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
}
|
|
3460
|
-
|
|
3562
|
+
const cb = mailer.config.circuitBreaker;
|
|
3563
|
+
const thresholds = {
|
|
3564
|
+
hardBounceRatePctTrip: cb.hardBounceRatePctTrip,
|
|
3565
|
+
complaintRatePctTrip: cb.complaintRatePctTrip,
|
|
3566
|
+
combinedBounceRatePctTrip: cb.combinedBounceRatePctTrip,
|
|
3567
|
+
failedToSendRatePctDegrade: cb.failedToSendRatePctDegrade
|
|
3568
|
+
};
|
|
3569
|
+
if (!h) {
|
|
3570
|
+
res.json({ status: null, rates: null, counters: null, thresholds });
|
|
3571
|
+
return;
|
|
3572
|
+
}
|
|
3573
|
+
res.json({ ...h, thresholds });
|
|
3574
|
+
})
|
|
3575
|
+
);
|
|
3576
|
+
r.get(
|
|
3577
|
+
"/health/trips",
|
|
3578
|
+
asyncHandler(async (_req, res) => {
|
|
3579
|
+
const rows = await c.auditLog.find({ action: { $in: ["health.trip", "health.resume"] } }).sort({ occurredAt: -1 }).limit(50).toArray();
|
|
3580
|
+
res.json(rows);
|
|
3461
3581
|
})
|
|
3462
3582
|
);
|
|
3463
3583
|
r.post(
|
|
@@ -3793,6 +3913,9 @@ function apiRouter(mailer) {
|
|
|
3793
3913
|
html = compiled.html;
|
|
3794
3914
|
plainText = compiled.plainText;
|
|
3795
3915
|
} else {
|
|
3916
|
+
if (!tpl.body?.html) {
|
|
3917
|
+
return res.status(409).json({ error: "not_published", message: "Template has not been published yet." });
|
|
3918
|
+
}
|
|
3796
3919
|
html = tpl.body.html;
|
|
3797
3920
|
plainText = tpl.body.plainText;
|
|
3798
3921
|
}
|
|
@@ -3924,8 +4047,15 @@ function apiRouter(mailer) {
|
|
|
3924
4047
|
if (f.kind === "hasTag") hostFilter.hasTag = f.tag;
|
|
3925
4048
|
if (f.kind === "fieldEquals") hostFilter.fieldEquals = { field: f.field, value: f.value };
|
|
3926
4049
|
}
|
|
3927
|
-
const
|
|
3928
|
-
|
|
4050
|
+
const hasMailerFilters = segmentDefinition.filters.some(
|
|
4051
|
+
(f) => ["subscriptionStatus", "firedEvent", "notFiredEvent", "notHasTag", "opened", "notOpened", "subscribedAfter", "subscribedBefore"].includes(f.kind)
|
|
4052
|
+
);
|
|
4053
|
+
const upperBound = await mailer.adapter.count(hostFilter);
|
|
4054
|
+
return res.json({
|
|
4055
|
+
upperBound,
|
|
4056
|
+
approximate: hasMailerFilters,
|
|
4057
|
+
computedMs: Date.now() - t0
|
|
4058
|
+
});
|
|
3929
4059
|
})
|
|
3930
4060
|
);
|
|
3931
4061
|
r.post(
|
|
@@ -3983,6 +4113,141 @@ function asyncHandler(fn) {
|
|
|
3983
4113
|
fn(req, res, next).catch(next);
|
|
3984
4114
|
};
|
|
3985
4115
|
}
|
|
4116
|
+
async function collectQueueCounts(mailer) {
|
|
4117
|
+
const qs = Object.values(mailer.queues);
|
|
4118
|
+
const sum = async (key) => {
|
|
4119
|
+
let total = 0;
|
|
4120
|
+
let supported = false;
|
|
4121
|
+
for (const q of qs) {
|
|
4122
|
+
const fn = q[key];
|
|
4123
|
+
if (!fn) continue;
|
|
4124
|
+
try {
|
|
4125
|
+
const v = await fn.call(q);
|
|
4126
|
+
if (v == null) continue;
|
|
4127
|
+
total += v;
|
|
4128
|
+
supported = true;
|
|
4129
|
+
} catch {
|
|
4130
|
+
return null;
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
4133
|
+
return supported ? total : null;
|
|
4134
|
+
};
|
|
4135
|
+
const [inFlight, delayed] = await Promise.all([sum("getInFlightCount"), sum("getDelayedCount")]);
|
|
4136
|
+
return { inFlight, delayed };
|
|
4137
|
+
}
|
|
4138
|
+
function emptyFlowStats() {
|
|
4139
|
+
return { activeRuns: 0, completedRuns: 0, sendsLast7Days: 0, sendsTotal: 0 };
|
|
4140
|
+
}
|
|
4141
|
+
async function computeFlowStats(mailer, slugFilter) {
|
|
4142
|
+
const out = /* @__PURE__ */ new Map();
|
|
4143
|
+
const match = slugFilter ? { flowSlug: slugFilter } : {};
|
|
4144
|
+
const runRows = await mailer.collections.flowRuns.aggregate([
|
|
4145
|
+
{ $match: match },
|
|
4146
|
+
{ $group: { _id: { flowSlug: "$flowSlug", status: "$status" }, count: { $sum: 1 } } }
|
|
4147
|
+
]).toArray();
|
|
4148
|
+
for (const row of runRows) {
|
|
4149
|
+
const slug = row._id.flowSlug;
|
|
4150
|
+
if (!slug) continue;
|
|
4151
|
+
const cur = out.get(slug) ?? emptyFlowStats();
|
|
4152
|
+
if (row._id.status === "active") cur.activeRuns += row.count;
|
|
4153
|
+
else if (row._id.status === "completed") cur.completedRuns += row.count;
|
|
4154
|
+
out.set(slug, cur);
|
|
4155
|
+
}
|
|
4156
|
+
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3);
|
|
4157
|
+
const sendRows = await mailer.collections.sends.aggregate([
|
|
4158
|
+
{ $match: { flowRunId: { $ne: null } } },
|
|
4159
|
+
{
|
|
4160
|
+
$lookup: {
|
|
4161
|
+
from: mailer.collections.flowRuns.collectionName,
|
|
4162
|
+
localField: "flowRunId",
|
|
4163
|
+
foreignField: "_id",
|
|
4164
|
+
as: "run",
|
|
4165
|
+
pipeline: slugFilter ? [{ $match: { flowSlug: slugFilter } }, { $project: { flowSlug: 1 } }] : [{ $project: { flowSlug: 1 } }]
|
|
4166
|
+
}
|
|
4167
|
+
},
|
|
4168
|
+
{ $unwind: "$run" },
|
|
4169
|
+
{
|
|
4170
|
+
$group: {
|
|
4171
|
+
_id: "$run.flowSlug",
|
|
4172
|
+
total: { $sum: 1 },
|
|
4173
|
+
last7: { $sum: { $cond: [{ $gte: ["$queuedAt", sevenDaysAgo] }, 1, 0] } }
|
|
4174
|
+
}
|
|
4175
|
+
}
|
|
4176
|
+
]).toArray();
|
|
4177
|
+
for (const row of sendRows) {
|
|
4178
|
+
if (!row._id) continue;
|
|
4179
|
+
const cur = out.get(row._id) ?? emptyFlowStats();
|
|
4180
|
+
cur.sendsTotal = row.total;
|
|
4181
|
+
cur.sendsLast7Days = row.last7;
|
|
4182
|
+
out.set(row._id, cur);
|
|
4183
|
+
}
|
|
4184
|
+
return out;
|
|
4185
|
+
}
|
|
4186
|
+
function emptyTemplateStats() {
|
|
4187
|
+
return { sent: 0, opened: 0, clicked: 0, bounced: 0, sentLast7Days: 0, lastSentAt: null };
|
|
4188
|
+
}
|
|
4189
|
+
async function computeTemplateStats(mailer, slugFilter) {
|
|
4190
|
+
const out = /* @__PURE__ */ new Map();
|
|
4191
|
+
const match = {};
|
|
4192
|
+
if (slugFilter) match.templateSlug = slugFilter;
|
|
4193
|
+
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3);
|
|
4194
|
+
const rows = await mailer.collections.sends.aggregate([
|
|
4195
|
+
{ $match: match },
|
|
4196
|
+
{
|
|
4197
|
+
$group: {
|
|
4198
|
+
_id: "$templateSlug",
|
|
4199
|
+
sent: { $sum: 1 },
|
|
4200
|
+
opened: { $sum: { $cond: [{ $ifNull: ["$openedAt", false] }, 1, 0] } },
|
|
4201
|
+
clicked: { $sum: { $cond: [{ $ifNull: ["$firstClickAt", false] }, 1, 0] } },
|
|
4202
|
+
bounced: { $sum: { $cond: [{ $eq: ["$status", "bounced"] }, 1, 0] } },
|
|
4203
|
+
sentLast7Days: { $sum: { $cond: [{ $gte: ["$queuedAt", sevenDaysAgo] }, 1, 0] } },
|
|
4204
|
+
lastSentAt: { $max: "$queuedAt" }
|
|
4205
|
+
}
|
|
4206
|
+
}
|
|
4207
|
+
]).toArray();
|
|
4208
|
+
for (const row of rows) {
|
|
4209
|
+
if (!row._id) continue;
|
|
4210
|
+
out.set(row._id, {
|
|
4211
|
+
sent: row.sent,
|
|
4212
|
+
opened: row.opened,
|
|
4213
|
+
clicked: row.clicked,
|
|
4214
|
+
bounced: row.bounced,
|
|
4215
|
+
sentLast7Days: row.sentLast7Days,
|
|
4216
|
+
lastSentAt: row.lastSentAt ?? null
|
|
4217
|
+
});
|
|
4218
|
+
}
|
|
4219
|
+
return out;
|
|
4220
|
+
}
|
|
4221
|
+
function emptyBroadcastStats() {
|
|
4222
|
+
return { delivered: 0, opened: 0, clicked: 0, bounced: 0 };
|
|
4223
|
+
}
|
|
4224
|
+
async function computeBroadcastStats(mailer, idFilter) {
|
|
4225
|
+
const out = /* @__PURE__ */ new Map();
|
|
4226
|
+
const match = { broadcastId: { $ne: null } };
|
|
4227
|
+
if (idFilter) match.broadcastId = idFilter;
|
|
4228
|
+
const rows = await mailer.collections.sends.aggregate([
|
|
4229
|
+
{ $match: match },
|
|
4230
|
+
{
|
|
4231
|
+
$group: {
|
|
4232
|
+
_id: "$broadcastId",
|
|
4233
|
+
delivered: { $sum: { $cond: [{ $eq: ["$status", "delivered"] }, 1, 0] } },
|
|
4234
|
+
opened: { $sum: { $cond: [{ $ifNull: ["$openedAt", false] }, 1, 0] } },
|
|
4235
|
+
clicked: { $sum: { $cond: [{ $ifNull: ["$firstClickAt", false] }, 1, 0] } },
|
|
4236
|
+
bounced: { $sum: { $cond: [{ $eq: ["$status", "bounced"] }, 1, 0] } }
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
]).toArray();
|
|
4240
|
+
for (const row of rows) {
|
|
4241
|
+
if (!row._id) continue;
|
|
4242
|
+
out.set(String(row._id), {
|
|
4243
|
+
delivered: row.delivered,
|
|
4244
|
+
opened: row.opened,
|
|
4245
|
+
clicked: row.clicked,
|
|
4246
|
+
bounced: row.bounced
|
|
4247
|
+
});
|
|
4248
|
+
}
|
|
4249
|
+
return out;
|
|
4250
|
+
}
|
|
3986
4251
|
var PIXEL = Buffer.from(
|
|
3987
4252
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
|
|
3988
4253
|
"base64"
|