cds-error-outbox 1.0.1 → 1.0.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/.github/workflows/publish.yml +29 -0
- package/README.md +311 -311
- package/config/defaults.js +46 -46
- package/db/model.cds +18 -18
- package/index.cds +1 -1
- package/index.js +14 -14
- package/lib/bootstrap.js +71 -71
- package/lib/config.js +67 -67
- package/lib/dedup.js +91 -91
- package/lib/formatter.js +214 -214
- package/lib/interceptor.js +59 -59
- package/lib/scheduler.js +125 -125
- package/package.json +40 -34
- package/providers/index.js +40 -40
- package/providers/mock.js +23 -23
- package/providers/o365.js +164 -164
- package/providers/smtp.js +71 -71
- package/tests/config.test.js +92 -0
- package/tests/dedup.test.js +159 -0
- package/tests/formatter.test.js +90 -0
- package/tests/scheduler.test.js +147 -0
package/lib/formatter.js
CHANGED
|
@@ -1,214 +1,214 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Escape special HTML characters to prevent injection in the email body.
|
|
5
|
-
*
|
|
6
|
-
* @param {string} str
|
|
7
|
-
* @returns {string}
|
|
8
|
-
*/
|
|
9
|
-
function escapeHtml(str) {
|
|
10
|
-
return String(str)
|
|
11
|
-
.replace(/&/g, '&')
|
|
12
|
-
.replace(/</g, '<')
|
|
13
|
-
.replace(/>/g, '>')
|
|
14
|
-
.replace(/"/g, '"')
|
|
15
|
-
.replace(/'/g, ''');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Build an HTML email body from an array of error records, grouped by service.
|
|
20
|
-
*
|
|
21
|
-
* @param {object[]} errors - array of error.outbox.Errors records
|
|
22
|
-
* @returns {{ subject: string, html: string }}
|
|
23
|
-
*/
|
|
24
|
-
function formatHtmlEmail(errors) {
|
|
25
|
-
const timestamp = new Date().toISOString();
|
|
26
|
-
const total = errors.reduce((sum, e) => sum + (e.count || 1), 0);
|
|
27
|
-
|
|
28
|
-
// Group records by service name
|
|
29
|
-
const groups = {};
|
|
30
|
-
for (const err of errors) {
|
|
31
|
-
const key = err.service || 'unknown';
|
|
32
|
-
if (!groups[key]) groups[key] = [];
|
|
33
|
-
groups[key].push(err);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const subject = `[CAP Error Outbox] ${total} occurrence(s) in ${errors.length} error(s) — ${timestamp}`;
|
|
37
|
-
|
|
38
|
-
const fmt = (ts) => ts ? String(ts).replace('T', ' ').replace(/\.\d{3}Z$/, ' UTC') : '-';
|
|
39
|
-
|
|
40
|
-
// ── Outlook-safe HTML email ──────────────────────────────────────────────
|
|
41
|
-
// Rules applied:
|
|
42
|
-
// 1. Every colored cell has BOTH bgcolor="" attribute AND style="background:"
|
|
43
|
-
// 2. Text colors use style="color:" on the immediate element (not parent)
|
|
44
|
-
// 3. No border-radius, no box-shadow, no gradients, no flexbox
|
|
45
|
-
// 4. display:block only via <p> tags, never on <span>
|
|
46
|
-
// 5. Table widths via width="" attribute, not just CSS
|
|
47
|
-
// 6. valign/align attributes used alongside CSS vertical-align/text-align
|
|
48
|
-
|
|
49
|
-
let html = `<!DOCTYPE html>
|
|
50
|
-
<html lang="en" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
|
51
|
-
<head>
|
|
52
|
-
<meta charset="UTF-8">
|
|
53
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
54
|
-
<meta name="color-scheme" content="light only">
|
|
55
|
-
<meta name="supported-color-schemes" content="light only">
|
|
56
|
-
<!--[if mso]><noscript><xml><o:OfficeDocumentSettings><o:AllowPNG/><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript><![endif]-->
|
|
57
|
-
<style>
|
|
58
|
-
body, table, td, p, a { -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; }
|
|
59
|
-
table, td { mso-table-lspace:0pt; mso-table-rspace:0pt; border-collapse:collapse; }
|
|
60
|
-
img { border:0; outline:none; text-decoration:none; }
|
|
61
|
-
/* Prevent Outlook dark mode */
|
|
62
|
-
[data-ogsc] body, [data-ogsb] body { background:#f4f4f4 !important; color:#222222 !important; }
|
|
63
|
-
</style>
|
|
64
|
-
</head>
|
|
65
|
-
<body style="margin:0;padding:0;background-color:#f4f4f4;" bgcolor="#f4f4f4">
|
|
66
|
-
|
|
67
|
-
<table width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="#f4f4f4" style="background-color:#f4f4f4;">
|
|
68
|
-
<tr>
|
|
69
|
-
<td align="center" style="padding:20px 10px;">
|
|
70
|
-
|
|
71
|
-
<!-- WRAPPER -->
|
|
72
|
-
<table width="700" cellpadding="0" cellspacing="0" border="0" style="width:700px;background-color:#ffffff;" bgcolor="#ffffff">
|
|
73
|
-
|
|
74
|
-
<!-- HEADER -->
|
|
75
|
-
<tr>
|
|
76
|
-
<td bgcolor="#b03a2e" style="background-color:#b03a2e;padding:24px 28px 20px;" align="left">
|
|
77
|
-
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:20px;font-weight:bold;color:#ffffff;line-height:1.2;">CAP Error Outbox Report</p>
|
|
78
|
-
<p style="margin:6px 0 0;font-family:Arial,Helvetica,sans-serif;font-size:11px;color:#f4c7c3;line-height:1;">Generated: ${escapeHtml(timestamp)}</p>
|
|
79
|
-
</td>
|
|
80
|
-
</tr>
|
|
81
|
-
|
|
82
|
-
<!-- STATS -->
|
|
83
|
-
<tr>
|
|
84
|
-
<td bgcolor="#ffffff" style="background-color:#ffffff;padding:0;border-bottom:2px solid #e8e8e8;">
|
|
85
|
-
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
|
86
|
-
<tr>
|
|
87
|
-
<td width="33%" align="center" bgcolor="#ffffff" style="background-color:#ffffff;padding:16px 10px;border-right:1px solid #e8e8e8;">
|
|
88
|
-
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:26px;font-weight:bold;color:#b03a2e;line-height:1;">${errors.length}</p>
|
|
89
|
-
<p style="margin:4px 0 0;font-family:Arial,Helvetica,sans-serif;font-size:10px;color:#888888;text-transform:uppercase;letter-spacing:0.5px;line-height:1;">Distinct Errors</p>
|
|
90
|
-
</td>
|
|
91
|
-
<td width="34%" align="center" bgcolor="#ffffff" style="background-color:#ffffff;padding:16px 10px;border-right:1px solid #e8e8e8;">
|
|
92
|
-
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:26px;font-weight:bold;color:#b03a2e;line-height:1;">${total}</p>
|
|
93
|
-
<p style="margin:4px 0 0;font-family:Arial,Helvetica,sans-serif;font-size:10px;color:#888888;text-transform:uppercase;letter-spacing:0.5px;line-height:1;">Total Occurrences</p>
|
|
94
|
-
</td>
|
|
95
|
-
<td width="33%" align="center" bgcolor="#ffffff" style="background-color:#ffffff;padding:16px 10px;">
|
|
96
|
-
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:26px;font-weight:bold;color:#b03a2e;line-height:1;">${Object.keys(groups).length}</p>
|
|
97
|
-
<p style="margin:4px 0 0;font-family:Arial,Helvetica,sans-serif;font-size:10px;color:#888888;text-transform:uppercase;letter-spacing:0.5px;line-height:1;">Services Affected</p>
|
|
98
|
-
</td>
|
|
99
|
-
</tr>
|
|
100
|
-
</table>
|
|
101
|
-
</td>
|
|
102
|
-
</tr>\n`;
|
|
103
|
-
|
|
104
|
-
for (const [service, serviceErrors] of Object.entries(groups)) {
|
|
105
|
-
html += `
|
|
106
|
-
<!-- SERVICE: ${escapeHtml(service)} -->
|
|
107
|
-
<tr>
|
|
108
|
-
<td bgcolor="#ffffff" style="background-color:#ffffff;padding:20px 28px 4px;">
|
|
109
|
-
|
|
110
|
-
<!-- Service label -->
|
|
111
|
-
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin-bottom:10px;">
|
|
112
|
-
<tr>
|
|
113
|
-
<td bgcolor="#f8f8f8" style="background-color:#f8f8f8;border-left:3px solid #b03a2e;padding:7px 12px;">
|
|
114
|
-
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:11px;font-weight:bold;color:#333333;text-transform:uppercase;letter-spacing:0.7px;">${escapeHtml(service)}</p>
|
|
115
|
-
</td>
|
|
116
|
-
</tr>
|
|
117
|
-
</table>
|
|
118
|
-
|
|
119
|
-
<!-- Error table -->
|
|
120
|
-
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="border:1px solid #e8e8e8;margin-bottom:16px;">
|
|
121
|
-
<!-- Table header -->
|
|
122
|
-
<tr>
|
|
123
|
-
<td width="24" align="center" bgcolor="#2c3e50" style="background-color:#2c3e50;padding:8px 6px;border-right:1px solid #3d5166;">
|
|
124
|
-
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:10px;font-weight:bold;color:#ffffff;text-transform:uppercase;">#</p>
|
|
125
|
-
</td>
|
|
126
|
-
<td width="110" bgcolor="#2c3e50" style="background-color:#2c3e50;padding:8px 10px;border-right:1px solid #3d5166;">
|
|
127
|
-
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:10px;font-weight:bold;color:#ffffff;text-transform:uppercase;letter-spacing:0.4px;">Action</p>
|
|
128
|
-
</td>
|
|
129
|
-
<td bgcolor="#2c3e50" style="background-color:#2c3e50;padding:8px 10px;border-right:1px solid #3d5166;">
|
|
130
|
-
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:10px;font-weight:bold;color:#ffffff;text-transform:uppercase;letter-spacing:0.4px;">Message</p>
|
|
131
|
-
</td>
|
|
132
|
-
<td width="44" align="center" bgcolor="#2c3e50" style="background-color:#2c3e50;padding:8px 6px;border-right:1px solid #3d5166;">
|
|
133
|
-
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:10px;font-weight:bold;color:#ffffff;text-transform:uppercase;">Cnt</p>
|
|
134
|
-
</td>
|
|
135
|
-
<td width="118" bgcolor="#2c3e50" style="background-color:#2c3e50;padding:8px 10px;border-right:1px solid #3d5166;">
|
|
136
|
-
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:10px;font-weight:bold;color:#ffffff;text-transform:uppercase;letter-spacing:0.4px;">First Seen</p>
|
|
137
|
-
</td>
|
|
138
|
-
<td width="118" bgcolor="#2c3e50" style="background-color:#2c3e50;padding:8px 10px;">
|
|
139
|
-
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:10px;font-weight:bold;color:#ffffff;text-transform:uppercase;letter-spacing:0.4px;">Last Seen</p>
|
|
140
|
-
</td>
|
|
141
|
-
</tr>\n`;
|
|
142
|
-
|
|
143
|
-
serviceErrors.forEach((err, idx) => {
|
|
144
|
-
const count = err.count || 1;
|
|
145
|
-
const rowBg = idx % 2 === 1 ? '#f9f9f9' : '#ffffff';
|
|
146
|
-
const bdrClr = '#e8e8e8';
|
|
147
|
-
|
|
148
|
-
html += ` <!-- Row ${idx + 1} -->
|
|
149
|
-
<tr>
|
|
150
|
-
<td align="center" valign="top" bgcolor="${rowBg}" style="background-color:${rowBg};padding:9px 6px;border-top:1px solid ${bdrClr};border-right:1px solid ${bdrClr};">
|
|
151
|
-
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:11px;color:#aaaaaa;">${idx + 1}</p>
|
|
152
|
-
</td>
|
|
153
|
-
<td valign="top" bgcolor="${rowBg}" style="background-color:${rowBg};padding:9px 10px;border-top:1px solid ${bdrClr};border-right:1px solid ${bdrClr};">
|
|
154
|
-
<p style="margin:0;font-family:Consolas,'Courier New',monospace;font-size:11px;color:#2c6fad;background-color:#eef4fb;padding:1px 5px;display:inline-block;">${escapeHtml(err.action || 'unknown')}</p>
|
|
155
|
-
</td>
|
|
156
|
-
<td valign="top" bgcolor="${rowBg}" style="background-color:${rowBg};padding:9px 10px;border-top:1px solid ${bdrClr};border-right:1px solid ${bdrClr};">
|
|
157
|
-
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;color:#b03a2e;">${escapeHtml(err.message || '-')}</p>
|
|
158
|
-
</td>
|
|
159
|
-
<td align="center" valign="top" bgcolor="${rowBg}" style="background-color:${rowBg};padding:9px 6px;border-top:1px solid ${bdrClr};border-right:1px solid ${bdrClr};">
|
|
160
|
-
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;color:#ffffff;background-color:${count >= 10 ? '#922b21' : '#c0392b'};padding:1px 7px;text-align:center;">${count}</p>
|
|
161
|
-
</td>
|
|
162
|
-
<td valign="top" bgcolor="${rowBg}" style="background-color:${rowBg};padding:9px 10px;border-top:1px solid ${bdrClr};border-right:1px solid ${bdrClr};">
|
|
163
|
-
<p style="margin:0;font-family:Consolas,'Courier New',monospace;font-size:10px;color:#888888;white-space:nowrap;">${escapeHtml(fmt(err.firstSeen))}</p>
|
|
164
|
-
</td>
|
|
165
|
-
<td valign="top" bgcolor="${rowBg}" style="background-color:${rowBg};padding:9px 10px;border-top:1px solid ${bdrClr};">
|
|
166
|
-
<p style="margin:0;font-family:Consolas,'Courier New',monospace;font-size:10px;color:#888888;white-space:nowrap;">${escapeHtml(fmt(err.lastSeen))}</p>
|
|
167
|
-
</td>
|
|
168
|
-
</tr>\n`;
|
|
169
|
-
|
|
170
|
-
// Stack trace as a separate full-width row
|
|
171
|
-
if (err.stack) {
|
|
172
|
-
const stackExcerpt = err.stack.split('\n').slice(0, 5).join('\n');
|
|
173
|
-
html += ` <tr>
|
|
174
|
-
<td valign="top" bgcolor="#fafafa" style="background-color:#fafafa;padding:0 0 0 24px;border-top:none;" colspan="6">
|
|
175
|
-
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
|
176
|
-
<tr>
|
|
177
|
-
<td bgcolor="#f2f2f2" style="background-color:#f2f2f2;padding:7px 10px;border-top:1px dashed #dddddd;border-left:3px solid #dddddd;">
|
|
178
|
-
<p style="margin:0;font-family:Consolas,'Courier New',monospace;font-size:10px;color:#666666;white-space:pre-wrap;line-height:1.5;">${escapeHtml(stackExcerpt)}</p>
|
|
179
|
-
</td>
|
|
180
|
-
</tr>
|
|
181
|
-
</table>
|
|
182
|
-
</td>
|
|
183
|
-
</tr>\n`;
|
|
184
|
-
}
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
html += ` </table>
|
|
188
|
-
|
|
189
|
-
</td>
|
|
190
|
-
</tr>\n`;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
html += `
|
|
194
|
-
<!-- FOOTER -->
|
|
195
|
-
<tr>
|
|
196
|
-
<td bgcolor="#f4f4f4" style="background-color:#f4f4f4;padding:14px 28px;border-top:1px solid #e8e8e8;" align="center">
|
|
197
|
-
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:10px;color:#aaaaaa;">Sent by <strong>cds-error-outbox</strong> — these errors are marked as sent and will not be repeated until new occurrences are captured.</p>
|
|
198
|
-
</td>
|
|
199
|
-
</tr>
|
|
200
|
-
|
|
201
|
-
</table>
|
|
202
|
-
<!-- /WRAPPER -->
|
|
203
|
-
|
|
204
|
-
</td>
|
|
205
|
-
</tr>
|
|
206
|
-
</table>
|
|
207
|
-
|
|
208
|
-
</body>
|
|
209
|
-
</html>`;
|
|
210
|
-
|
|
211
|
-
return { subject, html };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
module.exports = { formatHtmlEmail, escapeHtml };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Escape special HTML characters to prevent injection in the email body.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} str
|
|
7
|
+
* @returns {string}
|
|
8
|
+
*/
|
|
9
|
+
function escapeHtml(str) {
|
|
10
|
+
return String(str)
|
|
11
|
+
.replace(/&/g, '&')
|
|
12
|
+
.replace(/</g, '<')
|
|
13
|
+
.replace(/>/g, '>')
|
|
14
|
+
.replace(/"/g, '"')
|
|
15
|
+
.replace(/'/g, ''');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Build an HTML email body from an array of error records, grouped by service.
|
|
20
|
+
*
|
|
21
|
+
* @param {object[]} errors - array of error.outbox.Errors records
|
|
22
|
+
* @returns {{ subject: string, html: string }}
|
|
23
|
+
*/
|
|
24
|
+
function formatHtmlEmail(errors) {
|
|
25
|
+
const timestamp = new Date().toISOString();
|
|
26
|
+
const total = errors.reduce((sum, e) => sum + (e.count || 1), 0);
|
|
27
|
+
|
|
28
|
+
// Group records by service name
|
|
29
|
+
const groups = {};
|
|
30
|
+
for (const err of errors) {
|
|
31
|
+
const key = err.service || 'unknown';
|
|
32
|
+
if (!groups[key]) groups[key] = [];
|
|
33
|
+
groups[key].push(err);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const subject = `[CAP Error Outbox] ${total} occurrence(s) in ${errors.length} error(s) — ${timestamp}`;
|
|
37
|
+
|
|
38
|
+
const fmt = (ts) => ts ? String(ts).replace('T', ' ').replace(/\.\d{3}Z$/, ' UTC') : '-';
|
|
39
|
+
|
|
40
|
+
// ── Outlook-safe HTML email ──────────────────────────────────────────────
|
|
41
|
+
// Rules applied:
|
|
42
|
+
// 1. Every colored cell has BOTH bgcolor="" attribute AND style="background:"
|
|
43
|
+
// 2. Text colors use style="color:" on the immediate element (not parent)
|
|
44
|
+
// 3. No border-radius, no box-shadow, no gradients, no flexbox
|
|
45
|
+
// 4. display:block only via <p> tags, never on <span>
|
|
46
|
+
// 5. Table widths via width="" attribute, not just CSS
|
|
47
|
+
// 6. valign/align attributes used alongside CSS vertical-align/text-align
|
|
48
|
+
|
|
49
|
+
let html = `<!DOCTYPE html>
|
|
50
|
+
<html lang="en" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
|
51
|
+
<head>
|
|
52
|
+
<meta charset="UTF-8">
|
|
53
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
54
|
+
<meta name="color-scheme" content="light only">
|
|
55
|
+
<meta name="supported-color-schemes" content="light only">
|
|
56
|
+
<!--[if mso]><noscript><xml><o:OfficeDocumentSettings><o:AllowPNG/><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript><![endif]-->
|
|
57
|
+
<style>
|
|
58
|
+
body, table, td, p, a { -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; }
|
|
59
|
+
table, td { mso-table-lspace:0pt; mso-table-rspace:0pt; border-collapse:collapse; }
|
|
60
|
+
img { border:0; outline:none; text-decoration:none; }
|
|
61
|
+
/* Prevent Outlook dark mode */
|
|
62
|
+
[data-ogsc] body, [data-ogsb] body { background:#f4f4f4 !important; color:#222222 !important; }
|
|
63
|
+
</style>
|
|
64
|
+
</head>
|
|
65
|
+
<body style="margin:0;padding:0;background-color:#f4f4f4;" bgcolor="#f4f4f4">
|
|
66
|
+
|
|
67
|
+
<table width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="#f4f4f4" style="background-color:#f4f4f4;">
|
|
68
|
+
<tr>
|
|
69
|
+
<td align="center" style="padding:20px 10px;">
|
|
70
|
+
|
|
71
|
+
<!-- WRAPPER -->
|
|
72
|
+
<table width="700" cellpadding="0" cellspacing="0" border="0" style="width:700px;background-color:#ffffff;" bgcolor="#ffffff">
|
|
73
|
+
|
|
74
|
+
<!-- HEADER -->
|
|
75
|
+
<tr>
|
|
76
|
+
<td bgcolor="#b03a2e" style="background-color:#b03a2e;padding:24px 28px 20px;" align="left">
|
|
77
|
+
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:20px;font-weight:bold;color:#ffffff;line-height:1.2;">CAP Error Outbox Report</p>
|
|
78
|
+
<p style="margin:6px 0 0;font-family:Arial,Helvetica,sans-serif;font-size:11px;color:#f4c7c3;line-height:1;">Generated: ${escapeHtml(timestamp)}</p>
|
|
79
|
+
</td>
|
|
80
|
+
</tr>
|
|
81
|
+
|
|
82
|
+
<!-- STATS -->
|
|
83
|
+
<tr>
|
|
84
|
+
<td bgcolor="#ffffff" style="background-color:#ffffff;padding:0;border-bottom:2px solid #e8e8e8;">
|
|
85
|
+
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
|
86
|
+
<tr>
|
|
87
|
+
<td width="33%" align="center" bgcolor="#ffffff" style="background-color:#ffffff;padding:16px 10px;border-right:1px solid #e8e8e8;">
|
|
88
|
+
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:26px;font-weight:bold;color:#b03a2e;line-height:1;">${errors.length}</p>
|
|
89
|
+
<p style="margin:4px 0 0;font-family:Arial,Helvetica,sans-serif;font-size:10px;color:#888888;text-transform:uppercase;letter-spacing:0.5px;line-height:1;">Distinct Errors</p>
|
|
90
|
+
</td>
|
|
91
|
+
<td width="34%" align="center" bgcolor="#ffffff" style="background-color:#ffffff;padding:16px 10px;border-right:1px solid #e8e8e8;">
|
|
92
|
+
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:26px;font-weight:bold;color:#b03a2e;line-height:1;">${total}</p>
|
|
93
|
+
<p style="margin:4px 0 0;font-family:Arial,Helvetica,sans-serif;font-size:10px;color:#888888;text-transform:uppercase;letter-spacing:0.5px;line-height:1;">Total Occurrences</p>
|
|
94
|
+
</td>
|
|
95
|
+
<td width="33%" align="center" bgcolor="#ffffff" style="background-color:#ffffff;padding:16px 10px;">
|
|
96
|
+
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:26px;font-weight:bold;color:#b03a2e;line-height:1;">${Object.keys(groups).length}</p>
|
|
97
|
+
<p style="margin:4px 0 0;font-family:Arial,Helvetica,sans-serif;font-size:10px;color:#888888;text-transform:uppercase;letter-spacing:0.5px;line-height:1;">Services Affected</p>
|
|
98
|
+
</td>
|
|
99
|
+
</tr>
|
|
100
|
+
</table>
|
|
101
|
+
</td>
|
|
102
|
+
</tr>\n`;
|
|
103
|
+
|
|
104
|
+
for (const [service, serviceErrors] of Object.entries(groups)) {
|
|
105
|
+
html += `
|
|
106
|
+
<!-- SERVICE: ${escapeHtml(service)} -->
|
|
107
|
+
<tr>
|
|
108
|
+
<td bgcolor="#ffffff" style="background-color:#ffffff;padding:20px 28px 4px;">
|
|
109
|
+
|
|
110
|
+
<!-- Service label -->
|
|
111
|
+
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin-bottom:10px;">
|
|
112
|
+
<tr>
|
|
113
|
+
<td bgcolor="#f8f8f8" style="background-color:#f8f8f8;border-left:3px solid #b03a2e;padding:7px 12px;">
|
|
114
|
+
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:11px;font-weight:bold;color:#333333;text-transform:uppercase;letter-spacing:0.7px;">${escapeHtml(service)}</p>
|
|
115
|
+
</td>
|
|
116
|
+
</tr>
|
|
117
|
+
</table>
|
|
118
|
+
|
|
119
|
+
<!-- Error table -->
|
|
120
|
+
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="border:1px solid #e8e8e8;margin-bottom:16px;">
|
|
121
|
+
<!-- Table header -->
|
|
122
|
+
<tr>
|
|
123
|
+
<td width="24" align="center" bgcolor="#2c3e50" style="background-color:#2c3e50;padding:8px 6px;border-right:1px solid #3d5166;">
|
|
124
|
+
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:10px;font-weight:bold;color:#ffffff;text-transform:uppercase;">#</p>
|
|
125
|
+
</td>
|
|
126
|
+
<td width="110" bgcolor="#2c3e50" style="background-color:#2c3e50;padding:8px 10px;border-right:1px solid #3d5166;">
|
|
127
|
+
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:10px;font-weight:bold;color:#ffffff;text-transform:uppercase;letter-spacing:0.4px;">Action</p>
|
|
128
|
+
</td>
|
|
129
|
+
<td bgcolor="#2c3e50" style="background-color:#2c3e50;padding:8px 10px;border-right:1px solid #3d5166;">
|
|
130
|
+
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:10px;font-weight:bold;color:#ffffff;text-transform:uppercase;letter-spacing:0.4px;">Message</p>
|
|
131
|
+
</td>
|
|
132
|
+
<td width="44" align="center" bgcolor="#2c3e50" style="background-color:#2c3e50;padding:8px 6px;border-right:1px solid #3d5166;">
|
|
133
|
+
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:10px;font-weight:bold;color:#ffffff;text-transform:uppercase;">Cnt</p>
|
|
134
|
+
</td>
|
|
135
|
+
<td width="118" bgcolor="#2c3e50" style="background-color:#2c3e50;padding:8px 10px;border-right:1px solid #3d5166;">
|
|
136
|
+
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:10px;font-weight:bold;color:#ffffff;text-transform:uppercase;letter-spacing:0.4px;">First Seen</p>
|
|
137
|
+
</td>
|
|
138
|
+
<td width="118" bgcolor="#2c3e50" style="background-color:#2c3e50;padding:8px 10px;">
|
|
139
|
+
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:10px;font-weight:bold;color:#ffffff;text-transform:uppercase;letter-spacing:0.4px;">Last Seen</p>
|
|
140
|
+
</td>
|
|
141
|
+
</tr>\n`;
|
|
142
|
+
|
|
143
|
+
serviceErrors.forEach((err, idx) => {
|
|
144
|
+
const count = err.count || 1;
|
|
145
|
+
const rowBg = idx % 2 === 1 ? '#f9f9f9' : '#ffffff';
|
|
146
|
+
const bdrClr = '#e8e8e8';
|
|
147
|
+
|
|
148
|
+
html += ` <!-- Row ${idx + 1} -->
|
|
149
|
+
<tr>
|
|
150
|
+
<td align="center" valign="top" bgcolor="${rowBg}" style="background-color:${rowBg};padding:9px 6px;border-top:1px solid ${bdrClr};border-right:1px solid ${bdrClr};">
|
|
151
|
+
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:11px;color:#aaaaaa;">${idx + 1}</p>
|
|
152
|
+
</td>
|
|
153
|
+
<td valign="top" bgcolor="${rowBg}" style="background-color:${rowBg};padding:9px 10px;border-top:1px solid ${bdrClr};border-right:1px solid ${bdrClr};">
|
|
154
|
+
<p style="margin:0;font-family:Consolas,'Courier New',monospace;font-size:11px;color:#2c6fad;background-color:#eef4fb;padding:1px 5px;display:inline-block;">${escapeHtml(err.action || 'unknown')}</p>
|
|
155
|
+
</td>
|
|
156
|
+
<td valign="top" bgcolor="${rowBg}" style="background-color:${rowBg};padding:9px 10px;border-top:1px solid ${bdrClr};border-right:1px solid ${bdrClr};">
|
|
157
|
+
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;color:#b03a2e;">${escapeHtml(err.message || '-')}</p>
|
|
158
|
+
</td>
|
|
159
|
+
<td align="center" valign="top" bgcolor="${rowBg}" style="background-color:${rowBg};padding:9px 6px;border-top:1px solid ${bdrClr};border-right:1px solid ${bdrClr};">
|
|
160
|
+
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;color:#ffffff;background-color:${count >= 10 ? '#922b21' : '#c0392b'};padding:1px 7px;text-align:center;">${count}</p>
|
|
161
|
+
</td>
|
|
162
|
+
<td valign="top" bgcolor="${rowBg}" style="background-color:${rowBg};padding:9px 10px;border-top:1px solid ${bdrClr};border-right:1px solid ${bdrClr};">
|
|
163
|
+
<p style="margin:0;font-family:Consolas,'Courier New',monospace;font-size:10px;color:#888888;white-space:nowrap;">${escapeHtml(fmt(err.firstSeen))}</p>
|
|
164
|
+
</td>
|
|
165
|
+
<td valign="top" bgcolor="${rowBg}" style="background-color:${rowBg};padding:9px 10px;border-top:1px solid ${bdrClr};">
|
|
166
|
+
<p style="margin:0;font-family:Consolas,'Courier New',monospace;font-size:10px;color:#888888;white-space:nowrap;">${escapeHtml(fmt(err.lastSeen))}</p>
|
|
167
|
+
</td>
|
|
168
|
+
</tr>\n`;
|
|
169
|
+
|
|
170
|
+
// Stack trace as a separate full-width row
|
|
171
|
+
if (err.stack) {
|
|
172
|
+
const stackExcerpt = err.stack.split('\n').slice(0, 5).join('\n');
|
|
173
|
+
html += ` <tr>
|
|
174
|
+
<td valign="top" bgcolor="#fafafa" style="background-color:#fafafa;padding:0 0 0 24px;border-top:none;" colspan="6">
|
|
175
|
+
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
|
176
|
+
<tr>
|
|
177
|
+
<td bgcolor="#f2f2f2" style="background-color:#f2f2f2;padding:7px 10px;border-top:1px dashed #dddddd;border-left:3px solid #dddddd;">
|
|
178
|
+
<p style="margin:0;font-family:Consolas,'Courier New',monospace;font-size:10px;color:#666666;white-space:pre-wrap;line-height:1.5;">${escapeHtml(stackExcerpt)}</p>
|
|
179
|
+
</td>
|
|
180
|
+
</tr>
|
|
181
|
+
</table>
|
|
182
|
+
</td>
|
|
183
|
+
</tr>\n`;
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
html += ` </table>
|
|
188
|
+
|
|
189
|
+
</td>
|
|
190
|
+
</tr>\n`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
html += `
|
|
194
|
+
<!-- FOOTER -->
|
|
195
|
+
<tr>
|
|
196
|
+
<td bgcolor="#f4f4f4" style="background-color:#f4f4f4;padding:14px 28px;border-top:1px solid #e8e8e8;" align="center">
|
|
197
|
+
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:10px;color:#aaaaaa;">Sent by <strong>cds-error-outbox</strong> — these errors are marked as sent and will not be repeated until new occurrences are captured.</p>
|
|
198
|
+
</td>
|
|
199
|
+
</tr>
|
|
200
|
+
|
|
201
|
+
</table>
|
|
202
|
+
<!-- /WRAPPER -->
|
|
203
|
+
|
|
204
|
+
</td>
|
|
205
|
+
</tr>
|
|
206
|
+
</table>
|
|
207
|
+
|
|
208
|
+
</body>
|
|
209
|
+
</html>`;
|
|
210
|
+
|
|
211
|
+
return { subject, html };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
module.exports = { formatHtmlEmail, escapeHtml };
|
package/lib/interceptor.js
CHANGED
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const cds = require('@sap/cds');
|
|
4
|
-
const { upsertError } = require('./dedup');
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Attach a fire-and-forget error interceptor to the given CDS service.
|
|
8
|
-
* The handler never blocks or delays the request pipeline — errors are
|
|
9
|
-
* persisted to the DB asynchronously via setImmediate.
|
|
10
|
-
*
|
|
11
|
-
* @param {object} srv - CDS service instance
|
|
12
|
-
* @param {object} config - merged plugin config
|
|
13
|
-
*/
|
|
14
|
-
function attach(srv, config) {
|
|
15
|
-
// CAP's srv.handle() is the correct override point — it runs before/on/after
|
|
16
|
-
// handlers and any error thrown there propagates out of handle().
|
|
17
|
-
// dispatch() causes infinite recursion (it calls this.dispatch on sub-tx),
|
|
18
|
-
// srv.on('*') never fires when a before-handler throws.
|
|
19
|
-
// The CAP docs say: "Subclasses should overload handle instead of dispatch".
|
|
20
|
-
const _handle = srv.handle.bind(srv);
|
|
21
|
-
srv.handle = async function (req) {
|
|
22
|
-
try {
|
|
23
|
-
return await _handle(req);
|
|
24
|
-
} catch (err) {
|
|
25
|
-
// Persist asynchronously — never block or swallow the error.
|
|
26
|
-
setImmediate(async () => {
|
|
27
|
-
try {
|
|
28
|
-
const message = (err && err.message) ? String(err.message) : String(err);
|
|
29
|
-
const stack = (err && err.stack) ? String(err.stack) : '';
|
|
30
|
-
const service = srv.name || 'unknown';
|
|
31
|
-
const action =
|
|
32
|
-
(req && req.event) ? req.event :
|
|
33
|
-
(req && req.path) ? req.path :
|
|
34
|
-
(req && req.method) ? req.method :
|
|
35
|
-
'unknown';
|
|
36
|
-
|
|
37
|
-
// cds.tx() opens a brand-new root-level transaction that is completely
|
|
38
|
-
// independent of the failed request's already-rolled-back transaction.
|
|
39
|
-
// Without this, CAP's AsyncLocalStorage would propagate the dead
|
|
40
|
-
// transaction context into our INSERT/UPDATE, causing the
|
|
41
|
-
// "Transaction is rolled back" error.
|
|
42
|
-
await cds.tx(async (tx) => {
|
|
43
|
-
await upsertError(tx, { message, stack, service, action }, config);
|
|
44
|
-
});
|
|
45
|
-
} catch (internalError) {
|
|
46
|
-
// Never re-throw — log internally to avoid crashing the app.
|
|
47
|
-
console.error(
|
|
48
|
-
`[cds-error-outbox] Failed to persist error from service "${srv.name}":`,
|
|
49
|
-
internalError.message
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
throw err; // re-throw so CAP still sends the error response to the client
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
module.exports = { attach };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const cds = require('@sap/cds');
|
|
4
|
+
const { upsertError } = require('./dedup');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Attach a fire-and-forget error interceptor to the given CDS service.
|
|
8
|
+
* The handler never blocks or delays the request pipeline — errors are
|
|
9
|
+
* persisted to the DB asynchronously via setImmediate.
|
|
10
|
+
*
|
|
11
|
+
* @param {object} srv - CDS service instance
|
|
12
|
+
* @param {object} config - merged plugin config
|
|
13
|
+
*/
|
|
14
|
+
function attach(srv, config) {
|
|
15
|
+
// CAP's srv.handle() is the correct override point — it runs before/on/after
|
|
16
|
+
// handlers and any error thrown there propagates out of handle().
|
|
17
|
+
// dispatch() causes infinite recursion (it calls this.dispatch on sub-tx),
|
|
18
|
+
// srv.on('*') never fires when a before-handler throws.
|
|
19
|
+
// The CAP docs say: "Subclasses should overload handle instead of dispatch".
|
|
20
|
+
const _handle = srv.handle.bind(srv);
|
|
21
|
+
srv.handle = async function (req) {
|
|
22
|
+
try {
|
|
23
|
+
return await _handle(req);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
// Persist asynchronously — never block or swallow the error.
|
|
26
|
+
setImmediate(async () => {
|
|
27
|
+
try {
|
|
28
|
+
const message = (err && err.message) ? String(err.message) : String(err);
|
|
29
|
+
const stack = (err && err.stack) ? String(err.stack) : '';
|
|
30
|
+
const service = srv.name || 'unknown';
|
|
31
|
+
const action =
|
|
32
|
+
(req && req.event) ? req.event :
|
|
33
|
+
(req && req.path) ? req.path :
|
|
34
|
+
(req && req.method) ? req.method :
|
|
35
|
+
'unknown';
|
|
36
|
+
|
|
37
|
+
// cds.tx() opens a brand-new root-level transaction that is completely
|
|
38
|
+
// independent of the failed request's already-rolled-back transaction.
|
|
39
|
+
// Without this, CAP's AsyncLocalStorage would propagate the dead
|
|
40
|
+
// transaction context into our INSERT/UPDATE, causing the
|
|
41
|
+
// "Transaction is rolled back" error.
|
|
42
|
+
await cds.tx(async (tx) => {
|
|
43
|
+
await upsertError(tx, { message, stack, service, action }, config);
|
|
44
|
+
});
|
|
45
|
+
} catch (internalError) {
|
|
46
|
+
// Never re-throw — log internally to avoid crashing the app.
|
|
47
|
+
console.error(
|
|
48
|
+
`[cds-error-outbox] Failed to persist error from service "${srv.name}":`,
|
|
49
|
+
internalError.message
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
throw err; // re-throw so CAP still sends the error response to the client
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { attach };
|