cds-error-outbox 1.0.0 → 1.0.1
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/README.md +72 -44
- package/lib/interceptor.js +41 -30
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,19 +1,39 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# CDS Error Outbox
|
|
4
|
+
|
|
5
|
+
**Automatic error capture, deduplication & email alerting for SAP CAP applications.**
|
|
6
|
+
|
|
7
|
+
<br>
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/cds-error-outbox)
|
|
10
|
+
[](https://cap.cloud.sap)
|
|
11
|
+
[](./LICENSE)
|
|
12
|
+
[](#)
|
|
13
|
+
|
|
14
|
+
[](#o365--microsoft-graph-api)
|
|
15
|
+
[](#smtp)
|
|
16
|
+
[](#db-entity)
|
|
17
|
+
[](#db-entity)
|
|
18
|
+
[](#environment-variables)
|
|
19
|
+
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
**[Installation](#installation) · [Configuration](#configuration) · [Email Providers](#email-providers) · [How it works](#how-it-works) · [DB Entity](#db-entity) · [Environment variables](#environment-variables) · [Project structure](#project-structure)**
|
|
4
25
|
|
|
5
26
|
---
|
|
6
27
|
|
|
7
28
|
## Features
|
|
8
29
|
|
|
9
|
-
- Hooks into **all CAP services**
|
|
10
|
-
-
|
|
11
|
-
- **Deduplicates**
|
|
30
|
+
- Hooks into **all CAP services** automatically — zero manual wiring required
|
|
31
|
+
- Persists errors to a CDS-managed `error.outbox.Errors` entity
|
|
32
|
+
- **Deduplicates** via `SHA-256(message + service + action)` — increments a counter instead of flooding the DB
|
|
12
33
|
- Sends **batched HTML email reports** on a configurable interval
|
|
13
|
-
- Pluggable
|
|
14
|
-
-
|
|
15
|
-
- **
|
|
16
|
-
- Never crashes the application — all internal failures are logged and swallowed
|
|
34
|
+
- Pluggable providers: **O365 (Microsoft Graph)**, **SMTP (nodemailer)**, **Mock (dev/test)**
|
|
35
|
+
- **Non-blocking** — fire-and-forget capture, the request pipeline is never delayed
|
|
36
|
+
- **Resilient** — all internal failures are caught, logged, and swallowed; the app never crashes
|
|
17
37
|
|
|
18
38
|
---
|
|
19
39
|
|
|
@@ -25,12 +45,33 @@ npm install cds-error-outbox
|
|
|
25
45
|
|
|
26
46
|
Because `package.json` declares `"cds": { "plugin": true }`, CAP automatically loads `index.js` at startup.
|
|
27
47
|
|
|
28
|
-
> **
|
|
48
|
+
> **Note:** Due to how CAP resolves symlinked local packages, you may need to explicitly require the plugin so it is loaded at startup.
|
|
29
49
|
>
|
|
50
|
+
> **Option A — with a custom `server.js`:**
|
|
30
51
|
> ```js
|
|
31
|
-
> require(
|
|
52
|
+
> require("cds-error-outbox"); // ← add as the very first line
|
|
32
53
|
> // ... rest of your server.js
|
|
33
54
|
> ```
|
|
55
|
+
>
|
|
56
|
+
> **Option B — without a custom `server.js`** (most common):
|
|
57
|
+
> Add the require at the top of any service file, e.g. `srv/admin-service.js`:
|
|
58
|
+
> ```js
|
|
59
|
+
> require("cds-error-outbox"); // ← add at the top
|
|
60
|
+
> // ... rest of your service
|
|
61
|
+
> ```
|
|
62
|
+
|
|
63
|
+
Then register the DB model in your project (e.g. in `db/schema.cds`):
|
|
64
|
+
|
|
65
|
+
```cds
|
|
66
|
+
using from 'cds-error-outbox/db/model';
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
And deploy:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
cds deploy --to sqlite # local development
|
|
73
|
+
cds build # production (BTP, HANA)
|
|
74
|
+
```
|
|
34
75
|
|
|
35
76
|
---
|
|
36
77
|
|
|
@@ -66,24 +107,24 @@ Add the following to your project's `package.json` under `cds.requires`, or to `
|
|
|
66
107
|
|
|
67
108
|
### Configuration reference
|
|
68
109
|
|
|
69
|
-
| Key
|
|
70
|
-
|
|
71
|
-
| `enabled`
|
|
72
|
-
| `interval`
|
|
73
|
-
| `batchSize`
|
|
74
|
-
| `dedup.enabled`
|
|
75
|
-
| `dedup.windowMinutes` | number
|
|
76
|
-
| `mail.provider`
|
|
77
|
-
| `mail.from`
|
|
78
|
-
| `mail.to`
|
|
79
|
-
| `mail.tenantId`
|
|
80
|
-
| `mail.clientId`
|
|
81
|
-
| `mail.clientSecret`
|
|
82
|
-
| `mail.smtp.host`
|
|
83
|
-
| `mail.smtp.port`
|
|
84
|
-
| `mail.smtp.secure`
|
|
85
|
-
| `mail.smtp.auth.user` | string
|
|
86
|
-
| `mail.smtp.auth.pass` | string
|
|
110
|
+
| Key | Type | Default | Description |
|
|
111
|
+
| --------------------- | ----------- | -------- | -------------------------------------- |
|
|
112
|
+
| `enabled` | boolean | `true` | Enable/disable the entire plugin |
|
|
113
|
+
| `interval` | number (ms) | `300000` | Batch email job frequency |
|
|
114
|
+
| `batchSize` | number | `50` | Max errors per email batch |
|
|
115
|
+
| `dedup.enabled` | boolean | `true` | Enable hash-based deduplication |
|
|
116
|
+
| `dedup.windowMinutes` | number | `10` | Rolling dedup window in minutes |
|
|
117
|
+
| `mail.provider` | string | `'mock'` | `'o365'` \| `'smtp'` \| `'mock'` |
|
|
118
|
+
| `mail.from` | string | `''` | Sender address |
|
|
119
|
+
| `mail.to` | string | `''` | Recipient(s), comma-separated |
|
|
120
|
+
| `mail.tenantId` | string | `''` | Azure AD tenant ID (O365 only) |
|
|
121
|
+
| `mail.clientId` | string | `''` | Azure AD app client ID (O365 only) |
|
|
122
|
+
| `mail.clientSecret` | string | `''` | Azure AD app client secret (O365 only) |
|
|
123
|
+
| `mail.smtp.host` | string | `''` | SMTP host (SMTP only) |
|
|
124
|
+
| `mail.smtp.port` | number | `587` | SMTP port (SMTP only) |
|
|
125
|
+
| `mail.smtp.secure` | boolean | `false` | Use TLS (SMTP only) |
|
|
126
|
+
| `mail.smtp.auth.user` | string | `''` | SMTP username (SMTP only) |
|
|
127
|
+
| `mail.smtp.auth.pass` | string | `''` | SMTP password (SMTP only) |
|
|
87
128
|
|
|
88
129
|
---
|
|
89
130
|
|
|
@@ -163,7 +204,7 @@ npm install nodemailer
|
|
|
163
204
|
CAP service throws an error
|
|
164
205
|
│
|
|
165
206
|
▼
|
|
166
|
-
srv.
|
|
207
|
+
srv.handle() wrapper — fire-and-forget via setImmediate (non-blocking)
|
|
167
208
|
│
|
|
168
209
|
▼
|
|
169
210
|
SHA-256( message | service | action )
|
|
@@ -216,19 +257,6 @@ entity Errors {
|
|
|
216
257
|
}
|
|
217
258
|
```
|
|
218
259
|
|
|
219
|
-
After installing the plugin, register the model in your project's `db/` folder. Create a file `db/error-outbox.cds`:
|
|
220
|
-
|
|
221
|
-
```cds
|
|
222
|
-
using from '../plugins/cds-error-outbox/db/model';
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
Then run deploy:
|
|
226
|
-
|
|
227
|
-
```bash
|
|
228
|
-
cds deploy --to sqlite # local development
|
|
229
|
-
cds build # production (BTP, HANA)
|
|
230
|
-
```
|
|
231
|
-
|
|
232
260
|
---
|
|
233
261
|
|
|
234
262
|
## Environment variables
|
|
@@ -261,7 +289,7 @@ cds-error-outbox/
|
|
|
261
289
|
├── lib/
|
|
262
290
|
│ ├── bootstrap.js ← Init orchestration (cds lifecycle hooks)
|
|
263
291
|
│ ├── config.js ← Deep merge + config loader (singleton)
|
|
264
|
-
│ ├── interceptor.js ← srv.
|
|
292
|
+
│ ├── interceptor.js ← srv.handle() wrapper (fire-and-forget)
|
|
265
293
|
│ ├── dedup.js ← SHA-256 hash + DB upsert logic
|
|
266
294
|
│ ├── scheduler.js ← Interval batch job
|
|
267
295
|
│ └── formatter.js ← HTML email builder
|
package/lib/interceptor.js
CHANGED
|
@@ -12,37 +12,48 @@ const { upsertError } = require('./dedup');
|
|
|
12
12
|
* @param {object} config - merged plugin config
|
|
13
13
|
*/
|
|
14
14
|
function attach(srv, config) {
|
|
15
|
-
srv.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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';
|
|
28
36
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
+
};
|
|
46
57
|
}
|
|
47
58
|
|
|
48
59
|
module.exports = { attach };
|