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.cjs
CHANGED
|
@@ -831,6 +831,11 @@ function adaptBullQueue(q) {
|
|
|
831
831
|
return {
|
|
832
832
|
add: (name, data, opts) => q.add(name, data, opts),
|
|
833
833
|
getWaitingCount: () => q.getWaitingCount(),
|
|
834
|
+
getInFlightCount: async () => {
|
|
835
|
+
const [active, waiting] = await Promise.all([q.getActiveCount(), q.getWaitingCount()]);
|
|
836
|
+
return active + waiting;
|
|
837
|
+
},
|
|
838
|
+
getDelayedCount: () => q.getDelayedCount(),
|
|
834
839
|
close: () => q.close()
|
|
835
840
|
};
|
|
836
841
|
}
|
|
@@ -3261,43 +3266,131 @@ function createAdminRouter(mailer, opts = {}) {
|
|
|
3261
3266
|
function apiRouter(mailer) {
|
|
3262
3267
|
const r = express.Router();
|
|
3263
3268
|
const c = mailer.collections;
|
|
3264
|
-
r.get(
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3269
|
+
r.get(
|
|
3270
|
+
"/me",
|
|
3271
|
+
asyncHandler(async (req, res) => {
|
|
3272
|
+
const [flows, templates, broadcasts, contacts, suppressions, health] = await Promise.all([
|
|
3273
|
+
c.flows.estimatedDocumentCount(),
|
|
3274
|
+
c.templates.estimatedDocumentCount(),
|
|
3275
|
+
c.broadcasts.estimatedDocumentCount(),
|
|
3276
|
+
c.subscriptions.countDocuments({ status: "subscribed" }),
|
|
3277
|
+
c.suppressions.estimatedDocumentCount(),
|
|
3278
|
+
c.health.findOne({ _id: "singleton" })
|
|
3279
|
+
]);
|
|
3280
|
+
res.json({
|
|
3281
|
+
actor: req.actor,
|
|
3282
|
+
counts: { flows, templates, broadcasts, contacts, suppressions },
|
|
3283
|
+
health: { status: health?.status ?? null },
|
|
3284
|
+
providers: {
|
|
3285
|
+
names: Object.keys(mailer.config.providers),
|
|
3286
|
+
default: mailer.config.defaultProvider
|
|
3287
|
+
},
|
|
3288
|
+
broadcastConfirmationThreshold: mailer.config.broadcastConfirmationThreshold
|
|
3289
|
+
});
|
|
3290
|
+
})
|
|
3291
|
+
);
|
|
3270
3292
|
r.get(
|
|
3271
3293
|
"/dashboard",
|
|
3272
3294
|
asyncHandler(async (_req, res) => {
|
|
3273
|
-
const
|
|
3274
|
-
const
|
|
3295
|
+
const now = Date.now();
|
|
3296
|
+
const HOUR = 60 * 60 * 1e3;
|
|
3297
|
+
const since24h = new Date(now - 24 * HOUR);
|
|
3298
|
+
const since48h = new Date(now - 48 * HOUR);
|
|
3299
|
+
const [
|
|
3300
|
+
sentTotal,
|
|
3301
|
+
deliveredCount,
|
|
3302
|
+
bouncedCount,
|
|
3303
|
+
openedCount,
|
|
3304
|
+
clickedCount,
|
|
3305
|
+
sentPrev,
|
|
3306
|
+
deliveredPrev,
|
|
3307
|
+
openedPrev,
|
|
3308
|
+
clickedPrev,
|
|
3309
|
+
hourly
|
|
3310
|
+
] = await Promise.all([
|
|
3275
3311
|
c.sends.countDocuments({ queuedAt: { $gt: since24h } }),
|
|
3276
3312
|
c.sends.countDocuments({ queuedAt: { $gt: since24h }, status: "delivered" }),
|
|
3277
3313
|
c.sends.countDocuments({ queuedAt: { $gt: since24h }, status: "bounced" }),
|
|
3278
3314
|
c.sends.countDocuments({ queuedAt: { $gt: since24h }, openedAt: { $ne: null } }),
|
|
3279
|
-
c.sends.countDocuments({ queuedAt: { $gt: since24h }, firstClickAt: { $ne: null } })
|
|
3315
|
+
c.sends.countDocuments({ queuedAt: { $gt: since24h }, firstClickAt: { $ne: null } }),
|
|
3316
|
+
c.sends.countDocuments({ queuedAt: { $gt: since48h, $lte: since24h } }),
|
|
3317
|
+
c.sends.countDocuments({ queuedAt: { $gt: since48h, $lte: since24h }, status: "delivered" }),
|
|
3318
|
+
c.sends.countDocuments({ queuedAt: { $gt: since48h, $lte: since24h }, openedAt: { $ne: null } }),
|
|
3319
|
+
c.sends.countDocuments({ queuedAt: { $gt: since48h, $lte: since24h }, firstClickAt: { $ne: null } }),
|
|
3320
|
+
c.sends.aggregate([
|
|
3321
|
+
{ $match: { queuedAt: { $gt: since24h } } },
|
|
3322
|
+
{
|
|
3323
|
+
$project: {
|
|
3324
|
+
hour: {
|
|
3325
|
+
$toInt: {
|
|
3326
|
+
$divide: [{ $subtract: [now, { $toLong: "$queuedAt" }] }, HOUR]
|
|
3327
|
+
}
|
|
3328
|
+
},
|
|
3329
|
+
opened: { $cond: [{ $ifNull: ["$openedAt", false] }, 1, 0] }
|
|
3330
|
+
}
|
|
3331
|
+
},
|
|
3332
|
+
{ $group: { _id: "$hour", sends: { $sum: 1 }, opens: { $sum: "$opened" } } }
|
|
3333
|
+
]).toArray()
|
|
3280
3334
|
]);
|
|
3335
|
+
const sendSeries = new Array(24).fill(0);
|
|
3336
|
+
const openSeries = new Array(24).fill(0);
|
|
3337
|
+
for (const row of hourly) {
|
|
3338
|
+
const idx = 23 - Math.max(0, Math.min(23, row._id));
|
|
3339
|
+
sendSeries[idx] = row.sends;
|
|
3340
|
+
openSeries[idx] = row.opens;
|
|
3341
|
+
}
|
|
3342
|
+
const delta = (cur, prev) => {
|
|
3343
|
+
if (prev === 0) return null;
|
|
3344
|
+
return (cur - prev) / prev;
|
|
3345
|
+
};
|
|
3346
|
+
const rateDelta = (curN, curD, prevN, prevD) => {
|
|
3347
|
+
if (prevD === 0 || curD === 0) return null;
|
|
3348
|
+
return curN / curD - prevN / prevD;
|
|
3349
|
+
};
|
|
3281
3350
|
const health = await c.health.findOne({ _id: "singleton" });
|
|
3282
|
-
const
|
|
3351
|
+
const recentFlowsRaw = await c.flows.find({ enabled: true }).limit(5).toArray();
|
|
3352
|
+
const flowStatsMap = await computeFlowStats(mailer);
|
|
3353
|
+
const recentFlows = recentFlowsRaw.map((f) => ({ ...f, stats: flowStatsMap.get(f.slug) ?? emptyFlowStats() }));
|
|
3283
3354
|
const recentSends = await c.sends.find().sort({ queuedAt: -1 }).limit(6).toArray();
|
|
3284
3355
|
const recentAudit = await c.auditLog.find().sort({ occurredAt: -1 }).limit(5).toArray();
|
|
3356
|
+
const queueCounts = await collectQueueCounts(mailer);
|
|
3357
|
+
const lastSendError = await c.sends.findOne({ status: { $in: ["bounced", "failed"] } }, { sort: { queuedAt: -1 } });
|
|
3358
|
+
const lastSendOk = await c.sends.findOne({ status: "delivered" }, { sort: { queuedAt: -1 } });
|
|
3359
|
+
const providerOk = lastSendOk && lastSendError ? new Date(lastSendOk.queuedAt).getTime() >= new Date(lastSendError.queuedAt).getTime() : lastSendOk ? true : lastSendError ? false : null;
|
|
3285
3360
|
res.json({
|
|
3286
3361
|
kpis: {
|
|
3287
|
-
sends: { value: sentTotal, delta:
|
|
3362
|
+
sends: { value: sentTotal, delta: delta(sentTotal, sentPrev) },
|
|
3288
3363
|
deliveredRate: {
|
|
3289
|
-
value: sentTotal === 0 ?
|
|
3290
|
-
delta:
|
|
3364
|
+
value: sentTotal === 0 ? null : deliveredCount / sentTotal,
|
|
3365
|
+
delta: rateDelta(deliveredCount, sentTotal, deliveredPrev, sentPrev),
|
|
3291
3366
|
bounced: bouncedCount
|
|
3292
3367
|
},
|
|
3293
|
-
openRate: {
|
|
3294
|
-
|
|
3368
|
+
openRate: {
|
|
3369
|
+
value: sentTotal === 0 ? null : openedCount / sentTotal,
|
|
3370
|
+
delta: rateDelta(openedCount, sentTotal, openedPrev, sentPrev)
|
|
3371
|
+
},
|
|
3372
|
+
clickRate: {
|
|
3373
|
+
value: sentTotal === 0 ? null : clickedCount / sentTotal,
|
|
3374
|
+
delta: rateDelta(clickedCount, sentTotal, clickedPrev, sentPrev)
|
|
3375
|
+
}
|
|
3376
|
+
},
|
|
3377
|
+
series: { hourly: { sends: sendSeries, opens: openSeries } },
|
|
3378
|
+
health: {
|
|
3379
|
+
status: health?.status ?? null,
|
|
3380
|
+
rates: health?.rates ?? null,
|
|
3381
|
+
thresholds: {
|
|
3382
|
+
hardBounceRatePctTrip: mailer.config.circuitBreaker.hardBounceRatePctTrip,
|
|
3383
|
+
complaintRatePctTrip: mailer.config.circuitBreaker.complaintRatePctTrip,
|
|
3384
|
+
combinedBounceRatePctTrip: mailer.config.circuitBreaker.combinedBounceRatePctTrip,
|
|
3385
|
+
failedToSendRatePctDegrade: mailer.config.circuitBreaker.failedToSendRatePctDegrade
|
|
3386
|
+
}
|
|
3295
3387
|
},
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3388
|
+
queue: {
|
|
3389
|
+
inFlight: queueCounts?.inFlight ?? null,
|
|
3390
|
+
delayed: queueCounts?.delayed ?? null,
|
|
3391
|
+
providerOk,
|
|
3392
|
+
providerName: mailer.config.defaultProvider
|
|
3299
3393
|
},
|
|
3300
|
-
queue: { inFlight: 0, delayed: 0, providerOk: true, providerName: mailer.config.defaultProvider },
|
|
3301
3394
|
recentFlows,
|
|
3302
3395
|
recentSends,
|
|
3303
3396
|
recentAudit
|
|
@@ -3308,7 +3401,8 @@ function apiRouter(mailer) {
|
|
|
3308
3401
|
"/flows",
|
|
3309
3402
|
asyncHandler(async (_req, res) => {
|
|
3310
3403
|
const flows = await c.flows.find().sort({ updatedAt: -1 }).toArray();
|
|
3311
|
-
|
|
3404
|
+
const stats = await computeFlowStats(mailer);
|
|
3405
|
+
res.json(flows.map((f) => ({ ...f, stats: stats.get(f.slug) ?? emptyFlowStats() })));
|
|
3312
3406
|
})
|
|
3313
3407
|
);
|
|
3314
3408
|
r.get(
|
|
@@ -3316,7 +3410,8 @@ function apiRouter(mailer) {
|
|
|
3316
3410
|
asyncHandler(async (req, res) => {
|
|
3317
3411
|
const flow = await c.flows.findOne({ slug: req.params.slug });
|
|
3318
3412
|
if (!flow) return res.status(404).json({ error: "not_found" });
|
|
3319
|
-
|
|
3413
|
+
const stats = (await computeFlowStats(mailer, flow.slug)).get(flow.slug) ?? emptyFlowStats();
|
|
3414
|
+
return res.json({ ...flow, stats });
|
|
3320
3415
|
})
|
|
3321
3416
|
);
|
|
3322
3417
|
r.post(
|
|
@@ -3351,7 +3446,8 @@ function apiRouter(mailer) {
|
|
|
3351
3446
|
"/templates",
|
|
3352
3447
|
asyncHandler(async (_req, res) => {
|
|
3353
3448
|
const templates = await c.templates.find().sort({ updatedAt: -1 }).toArray();
|
|
3354
|
-
|
|
3449
|
+
const stats = await computeTemplateStats(mailer);
|
|
3450
|
+
res.json(templates.map((t) => ({ ...t, stats: stats.get(t.slug) ?? emptyTemplateStats() })));
|
|
3355
3451
|
})
|
|
3356
3452
|
);
|
|
3357
3453
|
r.get(
|
|
@@ -3359,14 +3455,16 @@ function apiRouter(mailer) {
|
|
|
3359
3455
|
asyncHandler(async (req, res) => {
|
|
3360
3456
|
const template = await c.templates.findOne({ slug: req.params.slug });
|
|
3361
3457
|
if (!template) return res.status(404).json({ error: "not_found" });
|
|
3362
|
-
|
|
3458
|
+
const stats = (await computeTemplateStats(mailer, template.slug)).get(template.slug) ?? emptyTemplateStats();
|
|
3459
|
+
return res.json({ ...template, stats });
|
|
3363
3460
|
})
|
|
3364
3461
|
);
|
|
3365
3462
|
r.get(
|
|
3366
3463
|
"/broadcasts",
|
|
3367
3464
|
asyncHandler(async (_req, res) => {
|
|
3368
3465
|
const broadcasts = await c.broadcasts.find().sort({ createdAt: -1 }).toArray();
|
|
3369
|
-
|
|
3466
|
+
const stats = await computeBroadcastStats(mailer);
|
|
3467
|
+
res.json(broadcasts.map((b) => ({ ...b, stats: stats.get(String(b._id)) ?? emptyBroadcastStats() })));
|
|
3370
3468
|
})
|
|
3371
3469
|
);
|
|
3372
3470
|
r.get(
|
|
@@ -3374,7 +3472,8 @@ function apiRouter(mailer) {
|
|
|
3374
3472
|
asyncHandler(async (req, res) => {
|
|
3375
3473
|
const broadcast = await c.broadcasts.findOne({ slug: req.params.slug });
|
|
3376
3474
|
if (!broadcast) return res.status(404).json({ error: "not_found" });
|
|
3377
|
-
|
|
3475
|
+
const stats = (await computeBroadcastStats(mailer, broadcast._id)).get(String(broadcast._id)) ?? emptyBroadcastStats();
|
|
3476
|
+
return res.json({ ...broadcast, stats });
|
|
3378
3477
|
})
|
|
3379
3478
|
);
|
|
3380
3479
|
r.get(
|
|
@@ -3382,8 +3481,20 @@ function apiRouter(mailer) {
|
|
|
3382
3481
|
asyncHandler(async (req, res) => {
|
|
3383
3482
|
const cursor = typeof req.query.cursor === "string" ? req.query.cursor : void 0;
|
|
3384
3483
|
const limit = Math.min(Number(req.query.limit ?? 50), 200);
|
|
3385
|
-
const { contacts, nextCursor } = await
|
|
3386
|
-
|
|
3484
|
+
const [{ contacts, nextCursor }, counts] = await Promise.all([
|
|
3485
|
+
mailer.adapter.query({}, { limit, cursor }),
|
|
3486
|
+
(async () => {
|
|
3487
|
+
const rows = await c.subscriptions.aggregate([{ $group: { _id: "$status", n: { $sum: 1 } } }]).toArray();
|
|
3488
|
+
const out = { subscribed: 0, pending_doi: 0, unsubscribed: 0, bounced: 0, complained: 0 };
|
|
3489
|
+
let total = 0;
|
|
3490
|
+
for (const r2 of rows) {
|
|
3491
|
+
if (r2._id) out[r2._id] = r2.n;
|
|
3492
|
+
total += r2.n;
|
|
3493
|
+
}
|
|
3494
|
+
return { ...out, total };
|
|
3495
|
+
})()
|
|
3496
|
+
]);
|
|
3497
|
+
res.json({ contacts, nextCursor, counts });
|
|
3387
3498
|
})
|
|
3388
3499
|
);
|
|
3389
3500
|
r.get(
|
|
@@ -3462,16 +3573,25 @@ function apiRouter(mailer) {
|
|
|
3462
3573
|
"/health",
|
|
3463
3574
|
asyncHandler(async (_req, res) => {
|
|
3464
3575
|
const h = await c.health.findOne({ _id: "singleton" });
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
}
|
|
3474
|
-
|
|
3576
|
+
const cb = mailer.config.circuitBreaker;
|
|
3577
|
+
const thresholds = {
|
|
3578
|
+
hardBounceRatePctTrip: cb.hardBounceRatePctTrip,
|
|
3579
|
+
complaintRatePctTrip: cb.complaintRatePctTrip,
|
|
3580
|
+
combinedBounceRatePctTrip: cb.combinedBounceRatePctTrip,
|
|
3581
|
+
failedToSendRatePctDegrade: cb.failedToSendRatePctDegrade
|
|
3582
|
+
};
|
|
3583
|
+
if (!h) {
|
|
3584
|
+
res.json({ status: null, rates: null, counters: null, thresholds });
|
|
3585
|
+
return;
|
|
3586
|
+
}
|
|
3587
|
+
res.json({ ...h, thresholds });
|
|
3588
|
+
})
|
|
3589
|
+
);
|
|
3590
|
+
r.get(
|
|
3591
|
+
"/health/trips",
|
|
3592
|
+
asyncHandler(async (_req, res) => {
|
|
3593
|
+
const rows = await c.auditLog.find({ action: { $in: ["health.trip", "health.resume"] } }).sort({ occurredAt: -1 }).limit(50).toArray();
|
|
3594
|
+
res.json(rows);
|
|
3475
3595
|
})
|
|
3476
3596
|
);
|
|
3477
3597
|
r.post(
|
|
@@ -3807,6 +3927,9 @@ function apiRouter(mailer) {
|
|
|
3807
3927
|
html = compiled.html;
|
|
3808
3928
|
plainText = compiled.plainText;
|
|
3809
3929
|
} else {
|
|
3930
|
+
if (!tpl.body?.html) {
|
|
3931
|
+
return res.status(409).json({ error: "not_published", message: "Template has not been published yet." });
|
|
3932
|
+
}
|
|
3810
3933
|
html = tpl.body.html;
|
|
3811
3934
|
plainText = tpl.body.plainText;
|
|
3812
3935
|
}
|
|
@@ -3938,8 +4061,15 @@ function apiRouter(mailer) {
|
|
|
3938
4061
|
if (f.kind === "hasTag") hostFilter.hasTag = f.tag;
|
|
3939
4062
|
if (f.kind === "fieldEquals") hostFilter.fieldEquals = { field: f.field, value: f.value };
|
|
3940
4063
|
}
|
|
3941
|
-
const
|
|
3942
|
-
|
|
4064
|
+
const hasMailerFilters = segmentDefinition.filters.some(
|
|
4065
|
+
(f) => ["subscriptionStatus", "firedEvent", "notFiredEvent", "notHasTag", "opened", "notOpened", "subscribedAfter", "subscribedBefore"].includes(f.kind)
|
|
4066
|
+
);
|
|
4067
|
+
const upperBound = await mailer.adapter.count(hostFilter);
|
|
4068
|
+
return res.json({
|
|
4069
|
+
upperBound,
|
|
4070
|
+
approximate: hasMailerFilters,
|
|
4071
|
+
computedMs: Date.now() - t0
|
|
4072
|
+
});
|
|
3943
4073
|
})
|
|
3944
4074
|
);
|
|
3945
4075
|
r.post(
|
|
@@ -3997,6 +4127,141 @@ function asyncHandler(fn) {
|
|
|
3997
4127
|
fn(req, res, next).catch(next);
|
|
3998
4128
|
};
|
|
3999
4129
|
}
|
|
4130
|
+
async function collectQueueCounts(mailer) {
|
|
4131
|
+
const qs = Object.values(mailer.queues);
|
|
4132
|
+
const sum = async (key) => {
|
|
4133
|
+
let total = 0;
|
|
4134
|
+
let supported = false;
|
|
4135
|
+
for (const q of qs) {
|
|
4136
|
+
const fn = q[key];
|
|
4137
|
+
if (!fn) continue;
|
|
4138
|
+
try {
|
|
4139
|
+
const v = await fn.call(q);
|
|
4140
|
+
if (v == null) continue;
|
|
4141
|
+
total += v;
|
|
4142
|
+
supported = true;
|
|
4143
|
+
} catch {
|
|
4144
|
+
return null;
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
return supported ? total : null;
|
|
4148
|
+
};
|
|
4149
|
+
const [inFlight, delayed] = await Promise.all([sum("getInFlightCount"), sum("getDelayedCount")]);
|
|
4150
|
+
return { inFlight, delayed };
|
|
4151
|
+
}
|
|
4152
|
+
function emptyFlowStats() {
|
|
4153
|
+
return { activeRuns: 0, completedRuns: 0, sendsLast7Days: 0, sendsTotal: 0 };
|
|
4154
|
+
}
|
|
4155
|
+
async function computeFlowStats(mailer, slugFilter) {
|
|
4156
|
+
const out = /* @__PURE__ */ new Map();
|
|
4157
|
+
const match = slugFilter ? { flowSlug: slugFilter } : {};
|
|
4158
|
+
const runRows = await mailer.collections.flowRuns.aggregate([
|
|
4159
|
+
{ $match: match },
|
|
4160
|
+
{ $group: { _id: { flowSlug: "$flowSlug", status: "$status" }, count: { $sum: 1 } } }
|
|
4161
|
+
]).toArray();
|
|
4162
|
+
for (const row of runRows) {
|
|
4163
|
+
const slug = row._id.flowSlug;
|
|
4164
|
+
if (!slug) continue;
|
|
4165
|
+
const cur = out.get(slug) ?? emptyFlowStats();
|
|
4166
|
+
if (row._id.status === "active") cur.activeRuns += row.count;
|
|
4167
|
+
else if (row._id.status === "completed") cur.completedRuns += row.count;
|
|
4168
|
+
out.set(slug, cur);
|
|
4169
|
+
}
|
|
4170
|
+
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3);
|
|
4171
|
+
const sendRows = await mailer.collections.sends.aggregate([
|
|
4172
|
+
{ $match: { flowRunId: { $ne: null } } },
|
|
4173
|
+
{
|
|
4174
|
+
$lookup: {
|
|
4175
|
+
from: mailer.collections.flowRuns.collectionName,
|
|
4176
|
+
localField: "flowRunId",
|
|
4177
|
+
foreignField: "_id",
|
|
4178
|
+
as: "run",
|
|
4179
|
+
pipeline: slugFilter ? [{ $match: { flowSlug: slugFilter } }, { $project: { flowSlug: 1 } }] : [{ $project: { flowSlug: 1 } }]
|
|
4180
|
+
}
|
|
4181
|
+
},
|
|
4182
|
+
{ $unwind: "$run" },
|
|
4183
|
+
{
|
|
4184
|
+
$group: {
|
|
4185
|
+
_id: "$run.flowSlug",
|
|
4186
|
+
total: { $sum: 1 },
|
|
4187
|
+
last7: { $sum: { $cond: [{ $gte: ["$queuedAt", sevenDaysAgo] }, 1, 0] } }
|
|
4188
|
+
}
|
|
4189
|
+
}
|
|
4190
|
+
]).toArray();
|
|
4191
|
+
for (const row of sendRows) {
|
|
4192
|
+
if (!row._id) continue;
|
|
4193
|
+
const cur = out.get(row._id) ?? emptyFlowStats();
|
|
4194
|
+
cur.sendsTotal = row.total;
|
|
4195
|
+
cur.sendsLast7Days = row.last7;
|
|
4196
|
+
out.set(row._id, cur);
|
|
4197
|
+
}
|
|
4198
|
+
return out;
|
|
4199
|
+
}
|
|
4200
|
+
function emptyTemplateStats() {
|
|
4201
|
+
return { sent: 0, opened: 0, clicked: 0, bounced: 0, sentLast7Days: 0, lastSentAt: null };
|
|
4202
|
+
}
|
|
4203
|
+
async function computeTemplateStats(mailer, slugFilter) {
|
|
4204
|
+
const out = /* @__PURE__ */ new Map();
|
|
4205
|
+
const match = {};
|
|
4206
|
+
if (slugFilter) match.templateSlug = slugFilter;
|
|
4207
|
+
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3);
|
|
4208
|
+
const rows = await mailer.collections.sends.aggregate([
|
|
4209
|
+
{ $match: match },
|
|
4210
|
+
{
|
|
4211
|
+
$group: {
|
|
4212
|
+
_id: "$templateSlug",
|
|
4213
|
+
sent: { $sum: 1 },
|
|
4214
|
+
opened: { $sum: { $cond: [{ $ifNull: ["$openedAt", false] }, 1, 0] } },
|
|
4215
|
+
clicked: { $sum: { $cond: [{ $ifNull: ["$firstClickAt", false] }, 1, 0] } },
|
|
4216
|
+
bounced: { $sum: { $cond: [{ $eq: ["$status", "bounced"] }, 1, 0] } },
|
|
4217
|
+
sentLast7Days: { $sum: { $cond: [{ $gte: ["$queuedAt", sevenDaysAgo] }, 1, 0] } },
|
|
4218
|
+
lastSentAt: { $max: "$queuedAt" }
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
]).toArray();
|
|
4222
|
+
for (const row of rows) {
|
|
4223
|
+
if (!row._id) continue;
|
|
4224
|
+
out.set(row._id, {
|
|
4225
|
+
sent: row.sent,
|
|
4226
|
+
opened: row.opened,
|
|
4227
|
+
clicked: row.clicked,
|
|
4228
|
+
bounced: row.bounced,
|
|
4229
|
+
sentLast7Days: row.sentLast7Days,
|
|
4230
|
+
lastSentAt: row.lastSentAt ?? null
|
|
4231
|
+
});
|
|
4232
|
+
}
|
|
4233
|
+
return out;
|
|
4234
|
+
}
|
|
4235
|
+
function emptyBroadcastStats() {
|
|
4236
|
+
return { delivered: 0, opened: 0, clicked: 0, bounced: 0 };
|
|
4237
|
+
}
|
|
4238
|
+
async function computeBroadcastStats(mailer, idFilter) {
|
|
4239
|
+
const out = /* @__PURE__ */ new Map();
|
|
4240
|
+
const match = { broadcastId: { $ne: null } };
|
|
4241
|
+
if (idFilter) match.broadcastId = idFilter;
|
|
4242
|
+
const rows = await mailer.collections.sends.aggregate([
|
|
4243
|
+
{ $match: match },
|
|
4244
|
+
{
|
|
4245
|
+
$group: {
|
|
4246
|
+
_id: "$broadcastId",
|
|
4247
|
+
delivered: { $sum: { $cond: [{ $eq: ["$status", "delivered"] }, 1, 0] } },
|
|
4248
|
+
opened: { $sum: { $cond: [{ $ifNull: ["$openedAt", false] }, 1, 0] } },
|
|
4249
|
+
clicked: { $sum: { $cond: [{ $ifNull: ["$firstClickAt", false] }, 1, 0] } },
|
|
4250
|
+
bounced: { $sum: { $cond: [{ $eq: ["$status", "bounced"] }, 1, 0] } }
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
]).toArray();
|
|
4254
|
+
for (const row of rows) {
|
|
4255
|
+
if (!row._id) continue;
|
|
4256
|
+
out.set(String(row._id), {
|
|
4257
|
+
delivered: row.delivered,
|
|
4258
|
+
opened: row.opened,
|
|
4259
|
+
clicked: row.clicked,
|
|
4260
|
+
bounced: row.bounced
|
|
4261
|
+
});
|
|
4262
|
+
}
|
|
4263
|
+
return out;
|
|
4264
|
+
}
|
|
4000
4265
|
var PIXEL = Buffer.from(
|
|
4001
4266
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
|
|
4002
4267
|
"base64"
|