Skip to content

Commit 0b5d80c

Browse files
committed
feat(node): Add Hapi Integration
1 parent 04e7be9 commit 0b5d80c

File tree

6 files changed

+483
-5
lines changed

6 files changed

+483
-5
lines changed

packages/node-experimental/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export {
5252
withScope,
5353
captureCheckIn,
5454
withMonitor,
55+
hapiErrorPlugin,
5556
} from '@sentry/node';
5657

5758
export type {

packages/node/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@
3030
"https-proxy-agent": "^5.0.0"
3131
},
3232
"devDependencies": {
33+
"@hapi/hapi": "^21.3.2",
3334
"@types/cookie": "0.5.2",
3435
"@types/express": "^4.17.14",
36+
"@types/hapi": "^18.0.13",
3537
"@types/lru-cache": "^5.1.0",
3638
"@types/node": "~10.17.0",
3739
"express": "^4.17.1",

packages/node/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,5 @@ const INTEGRATIONS = {
8989
};
9090

9191
export { INTEGRATIONS as Integrations, Handlers };
92+
93+
export { hapiErrorPlugin } from './integrations/hapi';
+163
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import type { Boom } from '@hapi/boom';
2+
import type { RequestEvent, ResponseObject, Server } from '@hapi/hapi';
3+
import { captureException, configureScope, continueTrace, getActiveTransaction, startTransaction } from '@sentry/core';
4+
import type { Integration } from '@sentry/types';
5+
import { addExceptionMechanism, dynamicSamplingContextToSentryBaggageHeader, fill } from '@sentry/utils';
6+
7+
function isResponseObject(response: ResponseObject | Boom): response is ResponseObject {
8+
return response && (response as ResponseObject).statusCode !== undefined;
9+
}
10+
11+
function isBoomObject(response: ResponseObject | Boom): response is Boom {
12+
return response && (response as Boom).isBoom !== undefined;
13+
}
14+
15+
function isErrorEvent(event: RequestEvent): event is RequestEvent {
16+
return event && (event as RequestEvent).error !== undefined;
17+
}
18+
19+
function sendErrorToSentry(errorData: object): void {
20+
captureException(errorData, scope => {
21+
scope.addEventProcessor(event => {
22+
addExceptionMechanism(event, {
23+
type: 'hapi',
24+
handled: false,
25+
data: {
26+
function: 'hapiErrorPlugin',
27+
},
28+
});
29+
return event;
30+
});
31+
32+
return scope;
33+
});
34+
}
35+
36+
export const hapiErrorPlugin = {
37+
name: 'SentryHapiErrorPlugin',
38+
version: '0.0.1',
39+
register: async function (server: Server) {
40+
server.events.on('request', (request, event) => {
41+
const transaction = getActiveTransaction();
42+
43+
if (isBoomObject(request.response)) {
44+
sendErrorToSentry(request.response);
45+
} else if (isErrorEvent(event)) {
46+
sendErrorToSentry(event.error);
47+
}
48+
49+
if (transaction) {
50+
transaction.setStatus('internal_error');
51+
transaction.finish();
52+
}
53+
});
54+
},
55+
};
56+
57+
export const hapiTracingPlugin = {
58+
name: 'SentryHapiTracingPlugin',
59+
version: '0.0.1',
60+
register: async function (server: Server) {
61+
server.ext('onPreHandler', (request, h) => {
62+
const transaction = continueTrace(
63+
{
64+
sentryTrace: request.headers['sentry-trace'] || undefined,
65+
baggage: request.headers['baggage'] || undefined,
66+
},
67+
transactionContext => {
68+
return startTransaction({
69+
...transactionContext,
70+
op: 'hapi.request',
71+
name: request.path,
72+
description: request.route ? request.route.path : '',
73+
});
74+
},
75+
);
76+
77+
configureScope(scope => {
78+
scope.setSpan(transaction);
79+
});
80+
81+
return h.continue;
82+
});
83+
84+
server.ext('onPreResponse', (request, h) => {
85+
const transaction = getActiveTransaction();
86+
87+
if (isResponseObject(request.response) && transaction) {
88+
const response = request.response as ResponseObject;
89+
response.header('sentry-trace', transaction.toTraceparent());
90+
91+
const dynamicSamplingContext = dynamicSamplingContextToSentryBaggageHeader(
92+
transaction.getDynamicSamplingContext(),
93+
);
94+
95+
if (dynamicSamplingContext) {
96+
response.header('sentry-baggage', dynamicSamplingContext);
97+
}
98+
}
99+
100+
return h.continue;
101+
});
102+
103+
server.ext('onPostHandler', (request, h) => {
104+
const transaction = getActiveTransaction();
105+
106+
if (isResponseObject(request.response) && transaction) {
107+
transaction.setHttpStatus(request.response.statusCode);
108+
}
109+
110+
if (transaction) {
111+
transaction.finish();
112+
}
113+
114+
return h.continue;
115+
});
116+
},
117+
};
118+
119+
export type HapiOptions = {
120+
/** Hapi server instance */
121+
server?: Server;
122+
};
123+
124+
/**
125+
* Hapi Framework Integration
126+
*/
127+
export class Hapi implements Integration {
128+
/**
129+
* @inheritDoc
130+
*/
131+
public static id: string = 'Hapi';
132+
133+
/**
134+
* @inheritDoc
135+
*/
136+
public name: string;
137+
138+
public _hapiServer: Server | undefined;
139+
140+
public constructor(options?: HapiOptions) {
141+
if (options?.server) {
142+
this._hapiServer = options.server;
143+
}
144+
145+
this.name = Hapi.id;
146+
}
147+
148+
/** @inheritDoc */
149+
public setupOnce(): void {
150+
if (!this._hapiServer) {
151+
return;
152+
}
153+
154+
fill(this._hapiServer, 'start', (originalStart: () => void) => {
155+
return async function (this: Server) {
156+
await this.register(hapiTracingPlugin);
157+
await this.register(hapiErrorPlugin);
158+
const result = originalStart.apply(this);
159+
return result;
160+
};
161+
});
162+
}
163+
}

packages/node/src/integrations/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export { Context } from './context';
88
export { RequestData } from './requestdata';
99
export { LocalVariables } from './localvariables';
1010
export { Undici } from './undici';
11+
export { Hapi } from './hapi';

0 commit comments

Comments
 (0)