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.js
CHANGED
|
@@ -3265,13 +3265,13 @@ function apiRouter(mailer) {
|
|
|
3265
3265
|
]);
|
|
3266
3266
|
res.json({
|
|
3267
3267
|
actor: req.actor,
|
|
3268
|
-
permissions: { canPublish: true, canSendBroadcasts: true, canManageSuppressions: true },
|
|
3269
3268
|
counts: { flows, templates, broadcasts, contacts, suppressions },
|
|
3270
|
-
health: { status: health?.status ??
|
|
3269
|
+
health: { status: health?.status ?? null },
|
|
3271
3270
|
providers: {
|
|
3272
3271
|
names: Object.keys(mailer.config.providers),
|
|
3273
3272
|
default: mailer.config.defaultProvider
|
|
3274
|
-
}
|
|
3273
|
+
},
|
|
3274
|
+
broadcastConfirmationThreshold: mailer.config.broadcastConfirmationThreshold
|
|
3275
3275
|
});
|
|
3276
3276
|
})
|
|
3277
3277
|
);
|
|
@@ -3334,7 +3334,9 @@ function apiRouter(mailer) {
|
|
|
3334
3334
|
return curN / curD - prevN / prevD;
|
|
3335
3335
|
};
|
|
3336
3336
|
const health = await c.health.findOne({ _id: "singleton" });
|
|
3337
|
-
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() }));
|
|
3338
3340
|
const recentSends = await c.sends.find().sort({ queuedAt: -1 }).limit(6).toArray();
|
|
3339
3341
|
const recentAudit = await c.auditLog.find().sort({ occurredAt: -1 }).limit(5).toArray();
|
|
3340
3342
|
const queueCounts = await collectQueueCounts(mailer);
|
|
@@ -3351,8 +3353,7 @@ function apiRouter(mailer) {
|
|
|
3351
3353
|
},
|
|
3352
3354
|
openRate: {
|
|
3353
3355
|
value: sentTotal === 0 ? null : openedCount / sentTotal,
|
|
3354
|
-
delta: rateDelta(openedCount, sentTotal, openedPrev, sentPrev)
|
|
3355
|
-
exclBots: false
|
|
3356
|
+
delta: rateDelta(openedCount, sentTotal, openedPrev, sentPrev)
|
|
3356
3357
|
},
|
|
3357
3358
|
clickRate: {
|
|
3358
3359
|
value: sentTotal === 0 ? null : clickedCount / sentTotal,
|
|
@@ -3360,7 +3361,16 @@ function apiRouter(mailer) {
|
|
|
3360
3361
|
}
|
|
3361
3362
|
},
|
|
3362
3363
|
series: { hourly: { sends: sendSeries, opens: openSeries } },
|
|
3363
|
-
health:
|
|
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
|
+
}
|
|
3373
|
+
},
|
|
3364
3374
|
queue: {
|
|
3365
3375
|
inFlight: queueCounts?.inFlight ?? null,
|
|
3366
3376
|
delayed: queueCounts?.delayed ?? null,
|
|
@@ -3377,7 +3387,8 @@ function apiRouter(mailer) {
|
|
|
3377
3387
|
"/flows",
|
|
3378
3388
|
asyncHandler(async (_req, res) => {
|
|
3379
3389
|
const flows = await c.flows.find().sort({ updatedAt: -1 }).toArray();
|
|
3380
|
-
|
|
3390
|
+
const stats = await computeFlowStats(mailer);
|
|
3391
|
+
res.json(flows.map((f) => ({ ...f, stats: stats.get(f.slug) ?? emptyFlowStats() })));
|
|
3381
3392
|
})
|
|
3382
3393
|
);
|
|
3383
3394
|
r.get(
|
|
@@ -3385,7 +3396,8 @@ function apiRouter(mailer) {
|
|
|
3385
3396
|
asyncHandler(async (req, res) => {
|
|
3386
3397
|
const flow = await c.flows.findOne({ slug: req.params.slug });
|
|
3387
3398
|
if (!flow) return res.status(404).json({ error: "not_found" });
|
|
3388
|
-
|
|
3399
|
+
const stats = (await computeFlowStats(mailer, flow.slug)).get(flow.slug) ?? emptyFlowStats();
|
|
3400
|
+
return res.json({ ...flow, stats });
|
|
3389
3401
|
})
|
|
3390
3402
|
);
|
|
3391
3403
|
r.post(
|
|
@@ -3420,7 +3432,8 @@ function apiRouter(mailer) {
|
|
|
3420
3432
|
"/templates",
|
|
3421
3433
|
asyncHandler(async (_req, res) => {
|
|
3422
3434
|
const templates = await c.templates.find().sort({ updatedAt: -1 }).toArray();
|
|
3423
|
-
|
|
3435
|
+
const stats = await computeTemplateStats(mailer);
|
|
3436
|
+
res.json(templates.map((t) => ({ ...t, stats: stats.get(t.slug) ?? emptyTemplateStats() })));
|
|
3424
3437
|
})
|
|
3425
3438
|
);
|
|
3426
3439
|
r.get(
|
|
@@ -3428,14 +3441,16 @@ function apiRouter(mailer) {
|
|
|
3428
3441
|
asyncHandler(async (req, res) => {
|
|
3429
3442
|
const template = await c.templates.findOne({ slug: req.params.slug });
|
|
3430
3443
|
if (!template) return res.status(404).json({ error: "not_found" });
|
|
3431
|
-
|
|
3444
|
+
const stats = (await computeTemplateStats(mailer, template.slug)).get(template.slug) ?? emptyTemplateStats();
|
|
3445
|
+
return res.json({ ...template, stats });
|
|
3432
3446
|
})
|
|
3433
3447
|
);
|
|
3434
3448
|
r.get(
|
|
3435
3449
|
"/broadcasts",
|
|
3436
3450
|
asyncHandler(async (_req, res) => {
|
|
3437
3451
|
const broadcasts = await c.broadcasts.find().sort({ createdAt: -1 }).toArray();
|
|
3438
|
-
|
|
3452
|
+
const stats = await computeBroadcastStats(mailer);
|
|
3453
|
+
res.json(broadcasts.map((b) => ({ ...b, stats: stats.get(String(b._id)) ?? emptyBroadcastStats() })));
|
|
3439
3454
|
})
|
|
3440
3455
|
);
|
|
3441
3456
|
r.get(
|
|
@@ -3443,7 +3458,8 @@ function apiRouter(mailer) {
|
|
|
3443
3458
|
asyncHandler(async (req, res) => {
|
|
3444
3459
|
const broadcast = await c.broadcasts.findOne({ slug: req.params.slug });
|
|
3445
3460
|
if (!broadcast) return res.status(404).json({ error: "not_found" });
|
|
3446
|
-
|
|
3461
|
+
const stats = (await computeBroadcastStats(mailer, broadcast._id)).get(String(broadcast._id)) ?? emptyBroadcastStats();
|
|
3462
|
+
return res.json({ ...broadcast, stats });
|
|
3447
3463
|
})
|
|
3448
3464
|
);
|
|
3449
3465
|
r.get(
|
|
@@ -3451,8 +3467,20 @@ function apiRouter(mailer) {
|
|
|
3451
3467
|
asyncHandler(async (req, res) => {
|
|
3452
3468
|
const cursor = typeof req.query.cursor === "string" ? req.query.cursor : void 0;
|
|
3453
3469
|
const limit = Math.min(Number(req.query.limit ?? 50), 200);
|
|
3454
|
-
const { contacts, nextCursor } = await
|
|
3455
|
-
|
|
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 });
|
|
3456
3484
|
})
|
|
3457
3485
|
);
|
|
3458
3486
|
r.get(
|
|
@@ -3531,16 +3559,18 @@ function apiRouter(mailer) {
|
|
|
3531
3559
|
"/health",
|
|
3532
3560
|
asyncHandler(async (_req, res) => {
|
|
3533
3561
|
const h = await c.health.findOne({ _id: "singleton" });
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
}
|
|
3543
|
-
|
|
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 });
|
|
3544
3574
|
})
|
|
3545
3575
|
);
|
|
3546
3576
|
r.get(
|
|
@@ -3883,6 +3913,9 @@ function apiRouter(mailer) {
|
|
|
3883
3913
|
html = compiled.html;
|
|
3884
3914
|
plainText = compiled.plainText;
|
|
3885
3915
|
} else {
|
|
3916
|
+
if (!tpl.body?.html) {
|
|
3917
|
+
return res.status(409).json({ error: "not_published", message: "Template has not been published yet." });
|
|
3918
|
+
}
|
|
3886
3919
|
html = tpl.body.html;
|
|
3887
3920
|
plainText = tpl.body.plainText;
|
|
3888
3921
|
}
|
|
@@ -4014,8 +4047,15 @@ function apiRouter(mailer) {
|
|
|
4014
4047
|
if (f.kind === "hasTag") hostFilter.hasTag = f.tag;
|
|
4015
4048
|
if (f.kind === "fieldEquals") hostFilter.fieldEquals = { field: f.field, value: f.value };
|
|
4016
4049
|
}
|
|
4017
|
-
const
|
|
4018
|
-
|
|
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
|
+
});
|
|
4019
4059
|
})
|
|
4020
4060
|
);
|
|
4021
4061
|
r.post(
|
|
@@ -4095,6 +4135,119 @@ async function collectQueueCounts(mailer) {
|
|
|
4095
4135
|
const [inFlight, delayed] = await Promise.all([sum("getInFlightCount"), sum("getDelayedCount")]);
|
|
4096
4136
|
return { inFlight, delayed };
|
|
4097
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
|
+
}
|
|
4098
4251
|
var PIXEL = Buffer.from(
|
|
4099
4252
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
|
|
4100
4253
|
"base64"
|