mailery 0.3.2 → 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-dMzrGcva.js → template-editor-DjkGIZF_.js} +107 -107
- package/dist/admin/spa/{template-editor-dMzrGcva.js.map → template-editor-DjkGIZF_.js.map} +1 -1
- package/dist/index.cjs +180 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +180 -27
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/admin/spa/index-B1Mp8lSz.js +0 -41
- package/dist/admin/spa/index-B1Mp8lSz.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -3279,13 +3279,13 @@ function apiRouter(mailer) {
|
|
|
3279
3279
|
]);
|
|
3280
3280
|
res.json({
|
|
3281
3281
|
actor: req.actor,
|
|
3282
|
-
permissions: { canPublish: true, canSendBroadcasts: true, canManageSuppressions: true },
|
|
3283
3282
|
counts: { flows, templates, broadcasts, contacts, suppressions },
|
|
3284
|
-
health: { status: health?.status ??
|
|
3283
|
+
health: { status: health?.status ?? null },
|
|
3285
3284
|
providers: {
|
|
3286
3285
|
names: Object.keys(mailer.config.providers),
|
|
3287
3286
|
default: mailer.config.defaultProvider
|
|
3288
|
-
}
|
|
3287
|
+
},
|
|
3288
|
+
broadcastConfirmationThreshold: mailer.config.broadcastConfirmationThreshold
|
|
3289
3289
|
});
|
|
3290
3290
|
})
|
|
3291
3291
|
);
|
|
@@ -3348,7 +3348,9 @@ function apiRouter(mailer) {
|
|
|
3348
3348
|
return curN / curD - prevN / prevD;
|
|
3349
3349
|
};
|
|
3350
3350
|
const health = await c.health.findOne({ _id: "singleton" });
|
|
3351
|
-
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() }));
|
|
3352
3354
|
const recentSends = await c.sends.find().sort({ queuedAt: -1 }).limit(6).toArray();
|
|
3353
3355
|
const recentAudit = await c.auditLog.find().sort({ occurredAt: -1 }).limit(5).toArray();
|
|
3354
3356
|
const queueCounts = await collectQueueCounts(mailer);
|
|
@@ -3365,8 +3367,7 @@ function apiRouter(mailer) {
|
|
|
3365
3367
|
},
|
|
3366
3368
|
openRate: {
|
|
3367
3369
|
value: sentTotal === 0 ? null : openedCount / sentTotal,
|
|
3368
|
-
delta: rateDelta(openedCount, sentTotal, openedPrev, sentPrev)
|
|
3369
|
-
exclBots: false
|
|
3370
|
+
delta: rateDelta(openedCount, sentTotal, openedPrev, sentPrev)
|
|
3370
3371
|
},
|
|
3371
3372
|
clickRate: {
|
|
3372
3373
|
value: sentTotal === 0 ? null : clickedCount / sentTotal,
|
|
@@ -3374,7 +3375,16 @@ function apiRouter(mailer) {
|
|
|
3374
3375
|
}
|
|
3375
3376
|
},
|
|
3376
3377
|
series: { hourly: { sends: sendSeries, opens: openSeries } },
|
|
3377
|
-
health:
|
|
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
|
+
}
|
|
3387
|
+
},
|
|
3378
3388
|
queue: {
|
|
3379
3389
|
inFlight: queueCounts?.inFlight ?? null,
|
|
3380
3390
|
delayed: queueCounts?.delayed ?? null,
|
|
@@ -3391,7 +3401,8 @@ function apiRouter(mailer) {
|
|
|
3391
3401
|
"/flows",
|
|
3392
3402
|
asyncHandler(async (_req, res) => {
|
|
3393
3403
|
const flows = await c.flows.find().sort({ updatedAt: -1 }).toArray();
|
|
3394
|
-
|
|
3404
|
+
const stats = await computeFlowStats(mailer);
|
|
3405
|
+
res.json(flows.map((f) => ({ ...f, stats: stats.get(f.slug) ?? emptyFlowStats() })));
|
|
3395
3406
|
})
|
|
3396
3407
|
);
|
|
3397
3408
|
r.get(
|
|
@@ -3399,7 +3410,8 @@ function apiRouter(mailer) {
|
|
|
3399
3410
|
asyncHandler(async (req, res) => {
|
|
3400
3411
|
const flow = await c.flows.findOne({ slug: req.params.slug });
|
|
3401
3412
|
if (!flow) return res.status(404).json({ error: "not_found" });
|
|
3402
|
-
|
|
3413
|
+
const stats = (await computeFlowStats(mailer, flow.slug)).get(flow.slug) ?? emptyFlowStats();
|
|
3414
|
+
return res.json({ ...flow, stats });
|
|
3403
3415
|
})
|
|
3404
3416
|
);
|
|
3405
3417
|
r.post(
|
|
@@ -3434,7 +3446,8 @@ function apiRouter(mailer) {
|
|
|
3434
3446
|
"/templates",
|
|
3435
3447
|
asyncHandler(async (_req, res) => {
|
|
3436
3448
|
const templates = await c.templates.find().sort({ updatedAt: -1 }).toArray();
|
|
3437
|
-
|
|
3449
|
+
const stats = await computeTemplateStats(mailer);
|
|
3450
|
+
res.json(templates.map((t) => ({ ...t, stats: stats.get(t.slug) ?? emptyTemplateStats() })));
|
|
3438
3451
|
})
|
|
3439
3452
|
);
|
|
3440
3453
|
r.get(
|
|
@@ -3442,14 +3455,16 @@ function apiRouter(mailer) {
|
|
|
3442
3455
|
asyncHandler(async (req, res) => {
|
|
3443
3456
|
const template = await c.templates.findOne({ slug: req.params.slug });
|
|
3444
3457
|
if (!template) return res.status(404).json({ error: "not_found" });
|
|
3445
|
-
|
|
3458
|
+
const stats = (await computeTemplateStats(mailer, template.slug)).get(template.slug) ?? emptyTemplateStats();
|
|
3459
|
+
return res.json({ ...template, stats });
|
|
3446
3460
|
})
|
|
3447
3461
|
);
|
|
3448
3462
|
r.get(
|
|
3449
3463
|
"/broadcasts",
|
|
3450
3464
|
asyncHandler(async (_req, res) => {
|
|
3451
3465
|
const broadcasts = await c.broadcasts.find().sort({ createdAt: -1 }).toArray();
|
|
3452
|
-
|
|
3466
|
+
const stats = await computeBroadcastStats(mailer);
|
|
3467
|
+
res.json(broadcasts.map((b) => ({ ...b, stats: stats.get(String(b._id)) ?? emptyBroadcastStats() })));
|
|
3453
3468
|
})
|
|
3454
3469
|
);
|
|
3455
3470
|
r.get(
|
|
@@ -3457,7 +3472,8 @@ function apiRouter(mailer) {
|
|
|
3457
3472
|
asyncHandler(async (req, res) => {
|
|
3458
3473
|
const broadcast = await c.broadcasts.findOne({ slug: req.params.slug });
|
|
3459
3474
|
if (!broadcast) return res.status(404).json({ error: "not_found" });
|
|
3460
|
-
|
|
3475
|
+
const stats = (await computeBroadcastStats(mailer, broadcast._id)).get(String(broadcast._id)) ?? emptyBroadcastStats();
|
|
3476
|
+
return res.json({ ...broadcast, stats });
|
|
3461
3477
|
})
|
|
3462
3478
|
);
|
|
3463
3479
|
r.get(
|
|
@@ -3465,8 +3481,20 @@ function apiRouter(mailer) {
|
|
|
3465
3481
|
asyncHandler(async (req, res) => {
|
|
3466
3482
|
const cursor = typeof req.query.cursor === "string" ? req.query.cursor : void 0;
|
|
3467
3483
|
const limit = Math.min(Number(req.query.limit ?? 50), 200);
|
|
3468
|
-
const { contacts, nextCursor } = await
|
|
3469
|
-
|
|
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 });
|
|
3470
3498
|
})
|
|
3471
3499
|
);
|
|
3472
3500
|
r.get(
|
|
@@ -3545,16 +3573,18 @@ function apiRouter(mailer) {
|
|
|
3545
3573
|
"/health",
|
|
3546
3574
|
asyncHandler(async (_req, res) => {
|
|
3547
3575
|
const h = await c.health.findOne({ _id: "singleton" });
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
}
|
|
3557
|
-
|
|
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 });
|
|
3558
3588
|
})
|
|
3559
3589
|
);
|
|
3560
3590
|
r.get(
|
|
@@ -3897,6 +3927,9 @@ function apiRouter(mailer) {
|
|
|
3897
3927
|
html = compiled.html;
|
|
3898
3928
|
plainText = compiled.plainText;
|
|
3899
3929
|
} else {
|
|
3930
|
+
if (!tpl.body?.html) {
|
|
3931
|
+
return res.status(409).json({ error: "not_published", message: "Template has not been published yet." });
|
|
3932
|
+
}
|
|
3900
3933
|
html = tpl.body.html;
|
|
3901
3934
|
plainText = tpl.body.plainText;
|
|
3902
3935
|
}
|
|
@@ -4028,8 +4061,15 @@ function apiRouter(mailer) {
|
|
|
4028
4061
|
if (f.kind === "hasTag") hostFilter.hasTag = f.tag;
|
|
4029
4062
|
if (f.kind === "fieldEquals") hostFilter.fieldEquals = { field: f.field, value: f.value };
|
|
4030
4063
|
}
|
|
4031
|
-
const
|
|
4032
|
-
|
|
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
|
+
});
|
|
4033
4073
|
})
|
|
4034
4074
|
);
|
|
4035
4075
|
r.post(
|
|
@@ -4109,6 +4149,119 @@ async function collectQueueCounts(mailer) {
|
|
|
4109
4149
|
const [inFlight, delayed] = await Promise.all([sum("getInFlightCount"), sum("getDelayedCount")]);
|
|
4110
4150
|
return { inFlight, delayed };
|
|
4111
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
|
+
}
|
|
4112
4265
|
var PIXEL = Buffer.from(
|
|
4113
4266
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
|
|
4114
4267
|
"base64"
|