Skip to content

Commit ceabe0f

Browse files
authored
Add support for events on 'app' channel and 'log' with 'error' tag (#32)
Add support for events on 'app' channel and 'log' with 'error' tag
2 parents c2b9b1c + 5edd9ad commit ceabe0f

File tree

4 files changed

+212
-25
lines changed

4 files changed

+212
-25
lines changed

README.md

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ request error logging to [Sentry](https://sentry.io/).
1313
Use the hapi plugin like this:
1414
```JavaScript
1515
const server = hapi.server();
16-
await server.register({
17-
plugin: require('hapi-sentry'),
16+
await server.register({
17+
plugin: require('hapi-sentry'),
1818
options: {
1919
client: { dsn: 'dsn-here' },
2020
},
@@ -25,26 +25,27 @@ await server.register({
2525

2626
The plugin options, you can pass in while registering are the following:
2727

28-
| property | type | description |
29-
|:--------------------------|:-------------|:-----------------------------------------------------------------------------------------------------------------------------|
30-
| `baseUri` | string | [uri](https://github.com/hapijs/joi/blob/master/API.md#stringurioptions) to be used as base for captured urls |
31-
| `trackUser` | boolean | Whether or not to track the user via the per-request scope. Default: `true` |
32-
| `scope.tags` | object | An array of tags to be sent with every event |
33-
| `scope.tags.name` | string | The name of a tag |
34-
| `scope.tags.value` | any | The value of a tag |
35-
| `scope.extra` | object | An object of arbitrary format to be sent as extra data on every event |
36-
| `client` | object | **required** A [@sentry/node](https://www.npmjs.com/package/@sentry/node) instance which was already initialized (using `Sentry.init`) OR an options object to be passed to an internally initialized [@sentry/node](https://www.npmjs.com/package/@sentry/node) (`client.dsn` is only required in the latter case) |
37-
| `client.dsn` | string/false | **required** The Dsn used to connect to Sentry and identify the project. If false, the SDK will not send any data to Sentry. |
38-
| `client.debug` | boolean | Turn debug mode on/off |
39-
| `client.release` | string | Tag events with the version/release identifier of your application |
40-
| `client.environment` | string | The current environment of your application (e.g. `'production'`) |
41-
| `client.sampleRate` | number | A global sample rate to apply to all events (0 - 1) |
42-
| `client.maxBreadcrumbs` | number | The maximum number of breadcrumbs sent with events. Default: `100` |
43-
| `client.attachStacktrace` | any | Attaches stacktraces to pure capture message / log integrations |
44-
| `client.sendDefaultPii` | boolean | If this flag is enabled, certain personally identifiable information is added by active integrations |
45-
| `client.serverName` | string | Overwrite the server name (device name) |
46-
| `client.beforeSend` | func | A callback invoked during event submission, allowing to optionally modify the event before it is sent to Sentry |
47-
| `client.beforeBreadcrumb` | func | A callback invoked when adding a breadcrumb, allowing to optionally modify it before adding it to future events. |
28+
| property | type | description |
29+
|:--------------------------|:--------------|:-----------------------------------------------------------------------------------------------------------------------------|
30+
| `baseUri` | string | [uri](https://github.com/hapijs/joi/blob/master/API.md#stringurioptions) to be used as base for captured urls |
31+
| `trackUser` | boolean | Whether or not to track the user via the per-request scope. Default: `true` |
32+
| `scope.tags` | object | An array of tags to be sent with every event |
33+
| `scope.tags.name` | string | The name of a tag |
34+
| `scope.tags.value` | any | The value of a tag |
35+
| `scope.extra` | object | An object of arbitrary format to be sent as extra data on every event |
36+
| `client` | object | **required** A [@sentry/node](https://www.npmjs.com/package/@sentry/node) instance which was already initialized (using `Sentry.init`) OR an options object to be passed to an internally initialized [@sentry/node](https://www.npmjs.com/package/@sentry/node) (`client.dsn` is only required in the latter case) |
37+
| `client.dsn` | string/false | **required** The Dsn used to connect to Sentry and identify the project. If false, the SDK will not send any data to Sentry. |
38+
| `client.debug` | boolean | Turn debug mode on/off |
39+
| `client.release` | string | Tag events with the version/release identifier of your application |
40+
| `client.environment` | string | The current environment of your application (e.g. `'production'`) |
41+
| `client.sampleRate` | number | A global sample rate to apply to all events (0 - 1) |
42+
| `client.maxBreadcrumbs` | number | The maximum number of breadcrumbs sent with events. Default: `100` |
43+
| `client.attachStacktrace` | any | Attaches stacktraces to pure capture message / log integrations |
44+
| `client.sendDefaultPii` | boolean | If this flag is enabled, certain personally identifiable information is added by active integrations |
45+
| `client.serverName` | string | Overwrite the server name (device name) |
46+
| `client.beforeSend` | func | A callback invoked during event submission, allowing to optionally modify the event before it is sent to Sentry |
47+
| `client.beforeBreadcrumb` | func | A callback invoked when adding a breadcrumb, allowing to optionally modify it before adding it to future events. |
48+
| `catchLogErrors` | boolean/array | Handles [capturing server.log and request.log events](#capturing-serverlog-and-requestlog-events). Default: `false` |
4849

4950
The `baseUri` option is used internally to get a correct URL in sentry issues.
5051
The `scope` option is used to set up a global
@@ -90,6 +91,20 @@ server.route({
9091
});
9192
```
9293

94+
## Capturing server.log and request.log events
95+
96+
You can enable capturing of `request.log` and `server.log` events using the `catchLogErrors` option.
97+
All events which are `Error` objects and are tagged by one of `['error', 'fatal', 'fail']` are
98+
automatically being tracked when `catchLogErrors` is set to `true`, e.g.:
99+
100+
```js
101+
request.log(['error', 'foo'], new Error('Oh no!'));
102+
server.log(['error', 'foo'], new Error('No no!'));
103+
```
104+
105+
The considered tags can be changed by setting `catchLogErrors` to a custom array of tags like
106+
`['error', 'warn', 'failure']`.
107+
93108
## Capturing the request body
94109

95110
`hapi-sentry` currently does not capture the body for performance reasons. You can use the following snippet to capture the body in all sentry errors:

index.js

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,23 @@ exports.register = (server, options) => {
3838
},
3939
});
4040

41+
let errorTags = ['error', 'fatal', 'fail'];
42+
if (opts.catchLogErrors && Array.isArray(opts.catchLogErrors)) {
43+
errorTags = opts.catchLogErrors;
44+
}
45+
46+
const channels = ['error'];
47+
// also listen for app events to get log messages
48+
if (opts.catchLogErrors) channels.push('app');
49+
4150
// get request errors to capture them with sentry
42-
server.events.on({ name: 'request', channels: ['error'] }, async (request, event) => {
51+
server.events.on({ name: 'request', channels }, (request, event) => {
52+
// check for errors in request logs
53+
if (event.channel === 'app') {
54+
if (!event.error) return; // no error, just a log message
55+
if (event.tags.some(tag => errorTags.includes(tag)) === false) return; // no matching tag
56+
}
57+
4358
Sentry.withScope(scope => { // thus use a temp scope and re-assign it
4459
scope.addEventProcessor(_sentryEvent => {
4560
// format a sentry event from the request and triggered event
@@ -51,8 +66,7 @@ exports.register = (server, options) => {
5166
sentryEvent.request.url = opts.baseUri + request.path;
5267
}
5368

54-
// set severity according to the filters channel
55-
sentryEvent.level = event.channel;
69+
sentryEvent.level = 'error';
5670

5771
// use request credentials for capturing user
5872
if (opts.trackUser) sentryEvent.user = request.auth && request.auth.credentials;
@@ -72,6 +86,24 @@ exports.register = (server, options) => {
7286
});
7387
});
7488

89+
if (opts.catchLogErrors) {
90+
server.events.on({ name: 'log', channels: ['app'] }, event => {
91+
if (!event.error) return; // no error, just a log message
92+
if (event.tags.some(tag => errorTags.includes(tag)) === false) return; // no matching tag
93+
94+
Sentry.withScope(scope => {
95+
scope.addEventProcessor(sentryEvent => {
96+
sentryEvent.level = 'error';
97+
98+
// some SDK identificator
99+
sentryEvent.sdk = { name: 'sentry.javascript.node.hapi', version };
100+
return sentryEvent;
101+
});
102+
103+
Sentry.captureException(event.error);
104+
});
105+
});
106+
}
75107
};
76108

77109
exports.name = name;

schema.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,8 @@ module.exports = joi.object().keys({
3232
extra: joi.object(),
3333
}),
3434
client: joi.alternatives().try([sentryOptions, sentryClient]).required(),
35+
catchLogErrors: joi.alternatives().try(
36+
joi.boolean(),
37+
joi.array().items(joi.string()),
38+
).default(false),
3539
});

test.js

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,3 +259,139 @@ test('sanitizes user info from auth', async t => {
259259
const event = await deferred.promise;
260260
t.deepEqual(event.user, { username: 'me' });
261261
});
262+
263+
test('process \'app\' channel events with default tags', async t => {
264+
const { server } = t.context;
265+
266+
server.route({
267+
method: 'GET',
268+
path: '/route',
269+
handler(request) {
270+
request.log(['error', 'foo'], new Error('Oh no!'));
271+
return null;
272+
},
273+
});
274+
275+
const deferred = defer();
276+
await server.register({
277+
plugin,
278+
options: {
279+
client: {
280+
dsn,
281+
beforeSend: deferred.resolve,
282+
},
283+
catchLogErrors: true,
284+
},
285+
});
286+
287+
await server.inject({
288+
method: 'GET',
289+
url: '/route',
290+
});
291+
292+
const event = await deferred.promise;
293+
t.is(event.exception.values[0].value, 'Oh no!');
294+
t.is(event.exception.values[0].type, 'Error');
295+
});
296+
297+
test('process \'app\' channel events with `catchLogErrors` tags', async t => {
298+
const { server } = t.context;
299+
300+
server.route({
301+
method: 'GET',
302+
path: '/route',
303+
handler(request) {
304+
request.log('exception', new Error('Oh no!'));
305+
return null;
306+
},
307+
});
308+
309+
const deferred = defer();
310+
await server.register({
311+
plugin,
312+
options: {
313+
client: {
314+
dsn,
315+
beforeSend: deferred.resolve,
316+
},
317+
catchLogErrors: ['exception', 'failure'],
318+
},
319+
});
320+
321+
await server.inject({
322+
method: 'GET',
323+
url: '/route',
324+
});
325+
326+
const event = await deferred.promise;
327+
t.is(event.exception.values[0].value, 'Oh no!');
328+
t.is(event.exception.values[0].type, 'Error');
329+
});
330+
331+
test('process \'log\' events with default tags', async t => {
332+
const { server } = t.context;
333+
334+
server.route({
335+
method: 'GET',
336+
path: '/route',
337+
handler() {
338+
server.log(['error', 'foo'], new Error('Oh no!'));
339+
return null;
340+
},
341+
});
342+
343+
const deferred = defer();
344+
await server.register({
345+
plugin,
346+
options: {
347+
client: {
348+
dsn,
349+
beforeSend: deferred.resolve,
350+
},
351+
catchLogErrors: true,
352+
},
353+
});
354+
355+
await server.inject({
356+
method: 'GET',
357+
url: '/route',
358+
});
359+
360+
const event = await deferred.promise;
361+
t.is(event.exception.values[0].value, 'Oh no!');
362+
t.is(event.exception.values[0].type, 'Error');
363+
});
364+
365+
test('process \'log\' events with `catchLogErrors` tags', async t => {
366+
const { server } = t.context;
367+
368+
server.route({
369+
method: 'GET',
370+
path: '/route',
371+
handler() {
372+
server.log('exception', new Error('Oh no!'));
373+
return null;
374+
},
375+
});
376+
377+
const deferred = defer();
378+
await server.register({
379+
plugin,
380+
options: {
381+
client: {
382+
dsn,
383+
beforeSend: deferred.resolve,
384+
},
385+
catchLogErrors: ['exception', 'failure'],
386+
},
387+
});
388+
389+
await server.inject({
390+
method: 'GET',
391+
url: '/route',
392+
});
393+
394+
const event = await deferred.promise;
395+
t.is(event.exception.values[0].value, 'Oh no!');
396+
t.is(event.exception.values[0].type, 'Error');
397+
});

0 commit comments

Comments
 (0)