Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions dev-packages/e2e-tests/test-applications/node-fastify/src/app.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -81,6 +81,16 @@ app.get('/test-error', async function (req, res) {
res.send({ exceptionId });
});

app.get('/test-4xx-error', async function (req, res) {
res.code(400);
throw new Error('This is a 4xx error');
});

app.get('/test-5xx-error', async function (req, res) {
res.code(500);
throw new Error('This is a 5xx error');
});

app.get<{ Params: { id: string } }>('/test-exception/:id', async function (req, res) {
throw new Error(`This is an exception with id ${req.params.id}`);
});
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,3 +28,25 @@ test('Sends correct error event', async ({ baseURL }) => {
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
});
});

test('Does not send 4xx errors by default', async ({ baseURL }) => {
// Define our test approach: we'll send both a 5xx and a 4xx request
// We should only see the 5xx error captured due to shouldHandleError's default behavior

// Create a promise to wait for the 500 error
const serverErrorPromise = waitForError('node-fastify', event => {
// Looking for a 500 error that should be captured
return !!event.exception?.values?.[0]?.value?.includes('This is a 5xx error');
});

// Make a request that will trigger a 400 error
const notFoundResponse = await fetch(`${baseURL}/test-4xx-error`);
expect(notFoundResponse.status).toBe(400);

// Make a request that will trigger a 500 error
await fetch(`${baseURL}/test-5xx-error`);

// Verify we receive the 500 error
const errorEvent = await serverErrorPromise;
expect(errorEvent.exception?.values?.[0]?.value).toContain('This is a 5xx error');
});
103 changes: 86 additions & 17 deletions packages/node/src/integrations/tracing/fastify.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,19 +12,14 @@ import type { IntegrationFn, Span } from '@sentry/core';
import { generateInstrumentOnce } from '../../otel/instrument';
import { ensureIsWrapped } from '../../utils/ensureIsWrapped';

// We inline the types we care about here
interface Fastify {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
register: (plugin: any) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
addHook: (hook: string, handler: (request: any, reply: any, error: Error) => void) => void;
}

/**
* Minimal request type containing properties around route information.
* Works for Fastify 3, 4 and presumably 5.
*
* Based on https://github.com/fastify/fastify/blob/ce3811f5f718be278bbcd4392c615d64230065a6/types/request.d.ts
*/
interface FastifyRequestRouteInfo {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
interface MinimalFastifyRequest extends Record<string, any> {
method?: string;
// since fastify@4.10.0
routeOptions?: {
Expand All@@ -33,6 +28,66 @@ interface FastifyRequestRouteInfo {
routerPath?: string;
}

/**
* Minimal reply type containing properties needed for error handling.
*
* Based on https://github.com/fastify/fastify/blob/ce3811f5f718be278bbcd4392c615d64230065a6/types/reply.d.ts
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
interface MinimalFastifyReply extends Record<string, any> {
statusCode: number;
}

// We inline the types we care about here
interface Fastify {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
register: (plugin: any) => void;
addHook: (hook: string, handler: (...params: unknown[]) => void) => void;
}

interface FastifyWithHooks extends Omit<Fastify, 'addHook'> {
addHook(
hook: 'onError',
handler: (request: MinimalFastifyRequest, reply: MinimalFastifyReply, error: Error) => void,
): void;
addHook(hook: 'onRequest', handler: (request: MinimalFastifyRequest, reply: MinimalFastifyReply) => void): void;
}

interface FastifyHandlerOptions {
/**
* Callback method deciding whether error should be captured and sent to Sentry
*
* @param error Captured Fastify error
* @param request Fastify request (or any object containing at least method, routeOptions.url, and routerPath)
* @param reply Fastify reply (or any object containing at least statusCode)
*
* @example
*
* ```javascript
* setupFastifyErrorHandler(app, {
* shouldHandleError(_error, _request, reply) {
* return reply.statusCode >= 400;
* },
* });
* ```
*
* If using TypeScript, you can cast the request and reply to get full type safety.
*
* ```typescript
* import type { FastifyRequest, FastifyReply } from 'fastify';
*
* setupFastifyErrorHandler(app, {
* shouldHandleError(error, minimalRequest, minimalReply) {
* const request = minimalRequest as FastifyRequest;
* const reply = minimalReply as FastifyReply;
* return reply.statusCode >= 500;
* },
* });
* ```
*/
shouldHandleError: (error: Error, request: MinimalFastifyRequest, reply: MinimalFastifyReply) => boolean;
}

const INTEGRATION_NAME = 'Fastify';

export const instrumentFastify = generateInstrumentOnce(
Expand DownExpand Up@@ -73,10 +128,22 @@ const _fastifyIntegration = (() => {
*/
export const fastifyIntegration = defineIntegration(_fastifyIntegration);

/**
* Default function to determine if an error should be sent to Sentry
*
* 3xx and 4xx errors are not sent by default.
*/
function defaultShouldHandleError(_error: Error, _request: MinimalFastifyRequest, reply: MinimalFastifyReply): boolean {
const statusCode = reply.statusCode;
// 3xx and 4xx errors are not sent by default.
return statusCode >= 500 || statusCode <= 299;
}

/**
* Add an Fastify error handler to capture errors to Sentry.
*
* @param fastify The Fastify instance to which to add the error handler
* @param options Configuration options for the handler
*
* @example
* ```javascript
Expand All@@ -92,23 +159,25 @@ export const fastifyIntegration = defineIntegration(_fastifyIntegration);
* app.listen({ port: 3000 });
* ```
*/
export function setupFastifyErrorHandler(fastify: Fastify): void {
export function setupFastifyErrorHandler(fastify: Fastify, options?: Partial<FastifyHandlerOptions>): void {
const shouldHandleError = options?.shouldHandleError || defaultShouldHandleError;

const plugin = Object.assign(
function (fastify: Fastify, _options: unknown, done: () => void): void {
fastify.addHook('onError', async (_request, _reply, error) => {
captureException(error);
function (fastify: FastifyWithHooks, _options: unknown, done: () => void): void {
fastify.addHook('onError', async (request, reply, error) => {
if (shouldHandleError(error, request, reply)) {
captureException(error);
}
});

// registering `onRequest` hook here instead of using Otel `onRequest` callback b/c `onRequest` hook
// is ironically called in the fastify `preHandler` hook which is called later in the lifecycle:
// https://fastify.dev/docs/latest/Reference/Lifecycle/
fastify.addHook('onRequest', async (request, _reply) => {
const reqWithRouteInfo = request as FastifyRequestRouteInfo;

// Taken from Otel Fastify instrumentation:
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-fastify/src/instrumentation.ts#L94-L96
const routeName = reqWithRouteInfo.routeOptions?.url || reqWithRouteInfo.routerPath;
const method = reqWithRouteInfo.method || 'GET';
const routeName = request.routeOptions?.url || request.routerPath;
const method = request.method || 'GET';

getIsolationScope().setTransactionName(`${method} ${routeName}`);
});
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat(node): Add fastify `shouldHandleError` by AbhiPrasad · Pull Request #15771 · getsentry/sentry-javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions dev-packages/e2e-tests/test-applications/node-fastify/src/app.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -81,6 +81,16 @@ app.get('/test-error', async function (req, res) {
res.send({ exceptionId });
});

app.get('/test-4xx-error', async function (req, res) {
res.code(400);
throw new Error('This is a 4xx error');
});

app.get('/test-5xx-error', async function (req, res) {
res.code(500);
throw new Error('This is a 5xx error');
});

app.get<{ Params: { id: string } }>('/test-exception/:id', async function (req, res) {
throw new Error(`This is an exception with id ${req.params.id}`);
});
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,3 +28,25 @@ test('Sends correct error event', async ({ baseURL }) => {
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
});
});

test('Does not send 4xx errors by default', async ({ baseURL }) => {
// Define our test approach: we'll send both a 5xx and a 4xx request
// We should only see the 5xx error captured due to shouldHandleError's default behavior

// Create a promise to wait for the 500 error
const serverErrorPromise = waitForError('node-fastify', event => {
// Looking for a 500 error that should be captured
return !!event.exception?.values?.[0]?.value?.includes('This is a 5xx error');
});

// Make a request that will trigger a 400 error
const notFoundResponse = await fetch(`${baseURL}/test-4xx-error`);
expect(notFoundResponse.status).toBe(400);

// Make a request that will trigger a 500 error
await fetch(`${baseURL}/test-5xx-error`);

// Verify we receive the 500 error
const errorEvent = await serverErrorPromise;
expect(errorEvent.exception?.values?.[0]?.value).toContain('This is a 5xx error');
});
103 changes: 86 additions & 17 deletions packages/node/src/integrations/tracing/fastify.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,19 +12,14 @@ import type { IntegrationFn, Span } from '@sentry/core';
import { generateInstrumentOnce } from '../../otel/instrument';
import { ensureIsWrapped } from '../../utils/ensureIsWrapped';

// We inline the types we care about here
interface Fastify {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
register: (plugin: any) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
addHook: (hook: string, handler: (request: any, reply: any, error: Error) => void) => void;
}

/**
* Minimal request type containing properties around route information.
* Works for Fastify 3, 4 and presumably 5.
*
* Based on https://github.com/fastify/fastify/blob/ce3811f5f718be278bbcd4392c615d64230065a6/types/request.d.ts
*/
interface FastifyRequestRouteInfo {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
interface MinimalFastifyRequest extends Record<string, any> {
method?: string;
// since fastify@4.10.0
routeOptions?: {
Expand All@@ -33,6 +28,66 @@ interface FastifyRequestRouteInfo {
routerPath?: string;
}

/**
* Minimal reply type containing properties needed for error handling.
*
* Based on https://github.com/fastify/fastify/blob/ce3811f5f718be278bbcd4392c615d64230065a6/types/reply.d.ts
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
interface MinimalFastifyReply extends Record<string, any> {
statusCode: number;
}

// We inline the types we care about here
interface Fastify {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
register: (plugin: any) => void;
addHook: (hook: string, handler: (...params: unknown[]) => void) => void;
}

interface FastifyWithHooks extends Omit<Fastify, 'addHook'> {
addHook(
hook: 'onError',
handler: (request: MinimalFastifyRequest, reply: MinimalFastifyReply, error: Error) => void,
): void;
addHook(hook: 'onRequest', handler: (request: MinimalFastifyRequest, reply: MinimalFastifyReply) => void): void;
}

interface FastifyHandlerOptions {
/**
* Callback method deciding whether error should be captured and sent to Sentry
*
* @param error Captured Fastify error
* @param request Fastify request (or any object containing at least method, routeOptions.url, and routerPath)
* @param reply Fastify reply (or any object containing at least statusCode)
*
* @example
*
* ```javascript
* setupFastifyErrorHandler(app, {
* shouldHandleError(_error, _request, reply) {
* return reply.statusCode >= 400;
* },
* });
* ```
*
* If using TypeScript, you can cast the request and reply to get full type safety.
*
* ```typescript
* import type { FastifyRequest, FastifyReply } from 'fastify';
*
* setupFastifyErrorHandler(app, {
* shouldHandleError(error, minimalRequest, minimalReply) {
* const request = minimalRequest as FastifyRequest;
* const reply = minimalReply as FastifyReply;
* return reply.statusCode >= 500;
* },
* });
* ```
*/
shouldHandleError: (error: Error, request: MinimalFastifyRequest, reply: MinimalFastifyReply) => boolean;
}

const INTEGRATION_NAME = 'Fastify';

export const instrumentFastify = generateInstrumentOnce(
Expand DownExpand Up@@ -73,10 +128,22 @@ const _fastifyIntegration = (() => {
*/
export const fastifyIntegration = defineIntegration(_fastifyIntegration);

/**
* Default function to determine if an error should be sent to Sentry
*
* 3xx and 4xx errors are not sent by default.
*/
function defaultShouldHandleError(_error: Error, _request: MinimalFastifyRequest, reply: MinimalFastifyReply): boolean {
const statusCode = reply.statusCode;
// 3xx and 4xx errors are not sent by default.
return statusCode >= 500 || statusCode <= 299;
}

/**
* Add an Fastify error handler to capture errors to Sentry.
*
* @param fastify The Fastify instance to which to add the error handler
* @param options Configuration options for the handler
*
* @example
* ```javascript
Expand All@@ -92,23 +159,25 @@ export const fastifyIntegration = defineIntegration(_fastifyIntegration);
* app.listen({ port: 3000 });
* ```
*/
export function setupFastifyErrorHandler(fastify: Fastify): void {
export function setupFastifyErrorHandler(fastify: Fastify, options?: Partial<FastifyHandlerOptions>): void {
const shouldHandleError = options?.shouldHandleError || defaultShouldHandleError;

const plugin = Object.assign(
function (fastify: Fastify, _options: unknown, done: () => void): void {
fastify.addHook('onError', async (_request, _reply, error) => {
captureException(error);
function (fastify: FastifyWithHooks, _options: unknown, done: () => void): void {
fastify.addHook('onError', async (request, reply, error) => {
if (shouldHandleError(error, request, reply)) {
captureException(error);
}
});

// registering `onRequest` hook here instead of using Otel `onRequest` callback b/c `onRequest` hook
// is ironically called in the fastify `preHandler` hook which is called later in the lifecycle:
// https://fastify.dev/docs/latest/Reference/Lifecycle/
fastify.addHook('onRequest', async (request, _reply) => {
const reqWithRouteInfo = request as FastifyRequestRouteInfo;

// Taken from Otel Fastify instrumentation:
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-fastify/src/instrumentation.ts#L94-L96
const routeName = reqWithRouteInfo.routeOptions?.url || reqWithRouteInfo.routerPath;
const method = reqWithRouteInfo.method || 'GET';
const routeName = request.routeOptions?.url || request.routerPath;
const method = request.method || 'GET';

getIsolationScope().setTransactionName(`${method} ${routeName}`);
});
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(node): Add fastify `shouldHandleError` by AbhiPrasad · Pull Request #15771 · getsentry/sentry-javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions dev-packages/e2e-tests/test-applications/node-fastify/src/app.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -81,6 +81,16 @@ app.get('/test-error', async function (req, res) {
res.send({ exceptionId });
});

app.get('/test-4xx-error', async function (req, res) {
res.code(400);
throw new Error('This is a 4xx error');
});

app.get('/test-5xx-error', async function (req, res) {
res.code(500);
throw new Error('This is a 5xx error');
});

app.get<{ Params: { id: string } }>('/test-exception/:id', async function (req, res) {
throw new Error(`This is an exception with id ${req.params.id}`);
});
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,3 +28,25 @@ test('Sends correct error event', async ({ baseURL }) => {
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
});
});

test('Does not send 4xx errors by default', async ({ baseURL }) => {
// Define our test approach: we'll send both a 5xx and a 4xx request
// We should only see the 5xx error captured due to shouldHandleError's default behavior

// Create a promise to wait for the 500 error
const serverErrorPromise = waitForError('node-fastify', event => {
// Looking for a 500 error that should be captured
return !!event.exception?.values?.[0]?.value?.includes('This is a 5xx error');
});

// Make a request that will trigger a 400 error
const notFoundResponse = await fetch(`${baseURL}/test-4xx-error`);
expect(notFoundResponse.status).toBe(400);

// Make a request that will trigger a 500 error
await fetch(`${baseURL}/test-5xx-error`);

// Verify we receive the 500 error
const errorEvent = await serverErrorPromise;
expect(errorEvent.exception?.values?.[0]?.value).toContain('This is a 5xx error');
});
103 changes: 86 additions & 17 deletions packages/node/src/integrations/tracing/fastify.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,19 +12,14 @@ import type { IntegrationFn, Span } from '@sentry/core';
import { generateInstrumentOnce } from '../../otel/instrument';
import { ensureIsWrapped } from '../../utils/ensureIsWrapped';

// We inline the types we care about here
interface Fastify {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
register: (plugin: any) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
addHook: (hook: string, handler: (request: any, reply: any, error: Error) => void) => void;
}

/**
* Minimal request type containing properties around route information.
* Works for Fastify 3, 4 and presumably 5.
*
* Based on https://github.com/fastify/fastify/blob/ce3811f5f718be278bbcd4392c615d64230065a6/types/request.d.ts
*/
interface FastifyRequestRouteInfo {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
interface MinimalFastifyRequest extends Record<string, any> {
method?: string;
// since fastify@4.10.0
routeOptions?: {
Expand All@@ -33,6 +28,66 @@ interface FastifyRequestRouteInfo {
routerPath?: string;
}

/**
* Minimal reply type containing properties needed for error handling.
*
* Based on https://github.com/fastify/fastify/blob/ce3811f5f718be278bbcd4392c615d64230065a6/types/reply.d.ts
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
interface MinimalFastifyReply extends Record<string, any> {
statusCode: number;
}

// We inline the types we care about here
interface Fastify {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
register: (plugin: any) => void;
addHook: (hook: string, handler: (...params: unknown[]) => void) => void;
}

interface FastifyWithHooks extends Omit<Fastify, 'addHook'> {
addHook(
hook: 'onError',
handler: (request: MinimalFastifyRequest, reply: MinimalFastifyReply, error: Error) => void,
): void;
addHook(hook: 'onRequest', handler: (request: MinimalFastifyRequest, reply: MinimalFastifyReply) => void): void;
}

interface FastifyHandlerOptions {
/**
* Callback method deciding whether error should be captured and sent to Sentry
*
* @param error Captured Fastify error
* @param request Fastify request (or any object containing at least method, routeOptions.url, and routerPath)
* @param reply Fastify reply (or any object containing at least statusCode)
*
* @example
*
* ```javascript
* setupFastifyErrorHandler(app, {
* shouldHandleError(_error, _request, reply) {
* return reply.statusCode >= 400;
* },
* });
* ```
*
* If using TypeScript, you can cast the request and reply to get full type safety.
*
* ```typescript
* import type { FastifyRequest, FastifyReply } from 'fastify';
*
* setupFastifyErrorHandler(app, {
* shouldHandleError(error, minimalRequest, minimalReply) {
* const request = minimalRequest as FastifyRequest;
* const reply = minimalReply as FastifyReply;
* return reply.statusCode >= 500;
* },
* });
* ```
*/
shouldHandleError: (error: Error, request: MinimalFastifyRequest, reply: MinimalFastifyReply) => boolean;
}

const INTEGRATION_NAME = 'Fastify';

export const instrumentFastify = generateInstrumentOnce(
Expand DownExpand Up@@ -73,10 +128,22 @@ const _fastifyIntegration = (() => {
*/
export const fastifyIntegration = defineIntegration(_fastifyIntegration);

/**
* Default function to determine if an error should be sent to Sentry
*
* 3xx and 4xx errors are not sent by default.
*/
function defaultShouldHandleError(_error: Error, _request: MinimalFastifyRequest, reply: MinimalFastifyReply): boolean {
const statusCode = reply.statusCode;
// 3xx and 4xx errors are not sent by default.
return statusCode >= 500 || statusCode <= 299;
}

/**
* Add an Fastify error handler to capture errors to Sentry.
*
* @param fastify The Fastify instance to which to add the error handler
* @param options Configuration options for the handler
*
* @example
* ```javascript
Expand All@@ -92,23 +159,25 @@ export const fastifyIntegration = defineIntegration(_fastifyIntegration);
* app.listen({ port: 3000 });
* ```
*/
export function setupFastifyErrorHandler(fastify: Fastify): void {
export function setupFastifyErrorHandler(fastify: Fastify, options?: Partial<FastifyHandlerOptions>): void {
const shouldHandleError = options?.shouldHandleError || defaultShouldHandleError;

const plugin = Object.assign(
function (fastify: Fastify, _options: unknown, done: () => void): void {
fastify.addHook('onError', async (_request, _reply, error) => {
captureException(error);
function (fastify: FastifyWithHooks, _options: unknown, done: () => void): void {
fastify.addHook('onError', async (request, reply, error) => {
if (shouldHandleError(error, request, reply)) {
captureException(error);
}
});

// registering `onRequest` hook here instead of using Otel `onRequest` callback b/c `onRequest` hook
// is ironically called in the fastify `preHandler` hook which is called later in the lifecycle:
// https://fastify.dev/docs/latest/Reference/Lifecycle/
fastify.addHook('onRequest', async (request, _reply) => {
const reqWithRouteInfo = request as FastifyRequestRouteInfo;

// Taken from Otel Fastify instrumentation:
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-fastify/src/instrumentation.ts#L94-L96
const routeName = reqWithRouteInfo.routeOptions?.url || reqWithRouteInfo.routerPath;
const method = reqWithRouteInfo.method || 'GET';
const routeName = request.routeOptions?.url || request.routerPath;
const method = request.method || 'GET';

getIsolationScope().setTransactionName(`${method} ${routeName}`);
});
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(node): Add fastify `shouldHandleError` by AbhiPrasad · Pull Request #15771 · getsentry/sentry-javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions dev-packages/e2e-tests/test-applications/node-fastify/src/app.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -81,6 +81,16 @@ app.get('/test-error', async function (req, res) {
res.send({ exceptionId });
});

app.get('/test-4xx-error', async function (req, res) {
res.code(400);
throw new Error('This is a 4xx error');
});

app.get('/test-5xx-error', async function (req, res) {
res.code(500);
throw new Error('This is a 5xx error');
});

app.get<{ Params: { id: string } }>('/test-exception/:id', async function (req, res) {
throw new Error(`This is an exception with id ${req.params.id}`);
});
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,3 +28,25 @@ test('Sends correct error event', async ({ baseURL }) => {
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
});
});

test('Does not send 4xx errors by default', async ({ baseURL }) => {
// Define our test approach: we'll send both a 5xx and a 4xx request
// We should only see the 5xx error captured due to shouldHandleError's default behavior

// Create a promise to wait for the 500 error
const serverErrorPromise = waitForError('node-fastify', event => {
// Looking for a 500 error that should be captured
return !!event.exception?.values?.[0]?.value?.includes('This is a 5xx error');
});

// Make a request that will trigger a 400 error
const notFoundResponse = await fetch(`${baseURL}/test-4xx-error`);
expect(notFoundResponse.status).toBe(400);

// Make a request that will trigger a 500 error
await fetch(`${baseURL}/test-5xx-error`);

// Verify we receive the 500 error
const errorEvent = await serverErrorPromise;
expect(errorEvent.exception?.values?.[0]?.value).toContain('This is a 5xx error');
});
103 changes: 86 additions & 17 deletions packages/node/src/integrations/tracing/fastify.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,19 +12,14 @@ import type { IntegrationFn, Span } from '@sentry/core';
import { generateInstrumentOnce } from '../../otel/instrument';
import { ensureIsWrapped } from '../../utils/ensureIsWrapped';

// We inline the types we care about here
interface Fastify {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
register: (plugin: any) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
addHook: (hook: string, handler: (request: any, reply: any, error: Error) => void) => void;
}

/**
* Minimal request type containing properties around route information.
* Works for Fastify 3, 4 and presumably 5.
*
* Based on https://github.com/fastify/fastify/blob/ce3811f5f718be278bbcd4392c615d64230065a6/types/request.d.ts
*/
interface FastifyRequestRouteInfo {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
interface MinimalFastifyRequest extends Record<string, any> {
method?: string;
// since fastify@4.10.0
routeOptions?: {
Expand All@@ -33,6 +28,66 @@ interface FastifyRequestRouteInfo {
routerPath?: string;
}

/**
* Minimal reply type containing properties needed for error handling.
*
* Based on https://github.com/fastify/fastify/blob/ce3811f5f718be278bbcd4392c615d64230065a6/types/reply.d.ts
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
interface MinimalFastifyReply extends Record<string, any> {
statusCode: number;
}

// We inline the types we care about here
interface Fastify {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
register: (plugin: any) => void;
addHook: (hook: string, handler: (...params: unknown[]) => void) => void;
}

interface FastifyWithHooks extends Omit<Fastify, 'addHook'> {
addHook(
hook: 'onError',
handler: (request: MinimalFastifyRequest, reply: MinimalFastifyReply, error: Error) => void,
): void;
addHook(hook: 'onRequest', handler: (request: MinimalFastifyRequest, reply: MinimalFastifyReply) => void): void;
}

interface FastifyHandlerOptions {
/**
* Callback method deciding whether error should be captured and sent to Sentry
*
* @param error Captured Fastify error
* @param request Fastify request (or any object containing at least method, routeOptions.url, and routerPath)
* @param reply Fastify reply (or any object containing at least statusCode)
*
* @example
*
* ```javascript
* setupFastifyErrorHandler(app, {
* shouldHandleError(_error, _request, reply) {
* return reply.statusCode >= 400;
* },
* });
* ```
*
* If using TypeScript, you can cast the request and reply to get full type safety.
*
* ```typescript
* import type { FastifyRequest, FastifyReply } from 'fastify';
*
* setupFastifyErrorHandler(app, {
* shouldHandleError(error, minimalRequest, minimalReply) {
* const request = minimalRequest as FastifyRequest;
* const reply = minimalReply as FastifyReply;
* return reply.statusCode >= 500;
* },
* });
* ```
*/
shouldHandleError: (error: Error, request: MinimalFastifyRequest, reply: MinimalFastifyReply) => boolean;
}

const INTEGRATION_NAME = 'Fastify';

export const instrumentFastify = generateInstrumentOnce(
Expand DownExpand Up@@ -73,10 +128,22 @@ const _fastifyIntegration = (() => {
*/
export const fastifyIntegration = defineIntegration(_fastifyIntegration);

/**
* Default function to determine if an error should be sent to Sentry
*
* 3xx and 4xx errors are not sent by default.
*/
function defaultShouldHandleError(_error: Error, _request: MinimalFastifyRequest, reply: MinimalFastifyReply): boolean {
const statusCode = reply.statusCode;
// 3xx and 4xx errors are not sent by default.
return statusCode >= 500 || statusCode <= 299;
}

/**
* Add an Fastify error handler to capture errors to Sentry.
*
* @param fastify The Fastify instance to which to add the error handler
* @param options Configuration options for the handler
*
* @example
* ```javascript
Expand All@@ -92,23 +159,25 @@ export const fastifyIntegration = defineIntegration(_fastifyIntegration);
* app.listen({ port: 3000 });
* ```
*/
export function setupFastifyErrorHandler(fastify: Fastify): void {
export function setupFastifyErrorHandler(fastify: Fastify, options?: Partial<FastifyHandlerOptions>): void {
const shouldHandleError = options?.shouldHandleError || defaultShouldHandleError;

const plugin = Object.assign(
function (fastify: Fastify, _options: unknown, done: () => void): void {
fastify.addHook('onError', async (_request, _reply, error) => {
captureException(error);
function (fastify: FastifyWithHooks, _options: unknown, done: () => void): void {
fastify.addHook('onError', async (request, reply, error) => {
if (shouldHandleError(error, request, reply)) {
captureException(error);
}
});

// registering `onRequest` hook here instead of using Otel `onRequest` callback b/c `onRequest` hook
// is ironically called in the fastify `preHandler` hook which is called later in the lifecycle:
// https://fastify.dev/docs/latest/Reference/Lifecycle/
fastify.addHook('onRequest', async (request, _reply) => {
const reqWithRouteInfo = request as FastifyRequestRouteInfo;

// Taken from Otel Fastify instrumentation:
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-fastify/src/instrumentation.ts#L94-L96
const routeName = reqWithRouteInfo.routeOptions?.url || reqWithRouteInfo.routerPath;
const method = reqWithRouteInfo.method || 'GET';
const routeName = request.routeOptions?.url || request.routerPath;
const method = request.method || 'GET';

getIsolationScope().setTransactionName(`${method} ${routeName}`);
});
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat(node): Add fastify `shouldHandleError` by AbhiPrasad · Pull Request #15771 · getsentry/sentry-javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions dev-packages/e2e-tests/test-applications/node-fastify/src/app.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -81,6 +81,16 @@ app.get('/test-error', async function (req, res) {
res.send({ exceptionId });
});

app.get('/test-4xx-error', async function (req, res) {
res.code(400);
throw new Error('This is a 4xx error');
});

app.get('/test-5xx-error', async function (req, res) {
res.code(500);
throw new Error('This is a 5xx error');
});

app.get<{ Params: { id: string } }>('/test-exception/:id', async function (req, res) {
throw new Error(`This is an exception with id ${req.params.id}`);
});
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,3 +28,25 @@ test('Sends correct error event', async ({ baseURL }) => {
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
});
});

test('Does not send 4xx errors by default', async ({ baseURL }) => {
// Define our test approach: we'll send both a 5xx and a 4xx request
// We should only see the 5xx error captured due to shouldHandleError's default behavior

// Create a promise to wait for the 500 error
const serverErrorPromise = waitForError('node-fastify', event => {
// Looking for a 500 error that should be captured
return !!event.exception?.values?.[0]?.value?.includes('This is a 5xx error');
});

// Make a request that will trigger a 400 error
const notFoundResponse = await fetch(`${baseURL}/test-4xx-error`);
expect(notFoundResponse.status).toBe(400);

// Make a request that will trigger a 500 error
await fetch(`${baseURL}/test-5xx-error`);

// Verify we receive the 500 error
const errorEvent = await serverErrorPromise;
expect(errorEvent.exception?.values?.[0]?.value).toContain('This is a 5xx error');
});
103 changes: 86 additions & 17 deletions packages/node/src/integrations/tracing/fastify.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,19 +12,14 @@ import type { IntegrationFn, Span } from '@sentry/core';
import { generateInstrumentOnce } from '../../otel/instrument';
import { ensureIsWrapped } from '../../utils/ensureIsWrapped';

// We inline the types we care about here
interface Fastify {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
register: (plugin: any) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
addHook: (hook: string, handler: (request: any, reply: any, error: Error) => void) => void;
}

/**
* Minimal request type containing properties around route information.
* Works for Fastify 3, 4 and presumably 5.
*
* Based on https://github.com/fastify/fastify/blob/ce3811f5f718be278bbcd4392c615d64230065a6/types/request.d.ts
*/
interface FastifyRequestRouteInfo {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
interface MinimalFastifyRequest extends Record<string, any> {
method?: string;
// since fastify@4.10.0
routeOptions?: {
Expand All@@ -33,6 +28,66 @@ interface FastifyRequestRouteInfo {
routerPath?: string;
}

/**
* Minimal reply type containing properties needed for error handling.
*
* Based on https://github.com/fastify/fastify/blob/ce3811f5f718be278bbcd4392c615d64230065a6/types/reply.d.ts
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
interface MinimalFastifyReply extends Record<string, any> {
statusCode: number;
}

// We inline the types we care about here
interface Fastify {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
register: (plugin: any) => void;
addHook: (hook: string, handler: (...params: unknown[]) => void) => void;
}

interface FastifyWithHooks extends Omit<Fastify, 'addHook'> {
addHook(
hook: 'onError',
handler: (request: MinimalFastifyRequest, reply: MinimalFastifyReply, error: Error) => void,
): void;
addHook(hook: 'onRequest', handler: (request: MinimalFastifyRequest, reply: MinimalFastifyReply) => void): void;
}

interface FastifyHandlerOptions {
/**
* Callback method deciding whether error should be captured and sent to Sentry
*
* @param error Captured Fastify error
* @param request Fastify request (or any object containing at least method, routeOptions.url, and routerPath)
* @param reply Fastify reply (or any object containing at least statusCode)
*
* @example
*
* ```javascript
* setupFastifyErrorHandler(app, {
* shouldHandleError(_error, _request, reply) {
* return reply.statusCode >= 400;
* },
* });
* ```
*
* If using TypeScript, you can cast the request and reply to get full type safety.
*
* ```typescript
* import type { FastifyRequest, FastifyReply } from 'fastify';
*
* setupFastifyErrorHandler(app, {
* shouldHandleError(error, minimalRequest, minimalReply) {
* const request = minimalRequest as FastifyRequest;
* const reply = minimalReply as FastifyReply;
* return reply.statusCode >= 500;
* },
* });
* ```
*/
shouldHandleError: (error: Error, request: MinimalFastifyRequest, reply: MinimalFastifyReply) => boolean;
}

const INTEGRATION_NAME = 'Fastify';

export const instrumentFastify = generateInstrumentOnce(
Expand DownExpand Up@@ -73,10 +128,22 @@ const _fastifyIntegration = (() => {
*/
export const fastifyIntegration = defineIntegration(_fastifyIntegration);

/**
* Default function to determine if an error should be sent to Sentry
*
* 3xx and 4xx errors are not sent by default.
*/
function defaultShouldHandleError(_error: Error, _request: MinimalFastifyRequest, reply: MinimalFastifyReply): boolean {
const statusCode = reply.statusCode;
// 3xx and 4xx errors are not sent by default.
return statusCode >= 500 || statusCode <= 299;
}

/**
* Add an Fastify error handler to capture errors to Sentry.
*
* @param fastify The Fastify instance to which to add the error handler
* @param options Configuration options for the handler
*
* @example
* ```javascript
Expand All@@ -92,23 +159,25 @@ export const fastifyIntegration = defineIntegration(_fastifyIntegration);
* app.listen({ port: 3000 });
* ```
*/
export function setupFastifyErrorHandler(fastify: Fastify): void {
export function setupFastifyErrorHandler(fastify: Fastify, options?: Partial<FastifyHandlerOptions>): void {
const shouldHandleError = options?.shouldHandleError || defaultShouldHandleError;

const plugin = Object.assign(
function (fastify: Fastify, _options: unknown, done: () => void): void {
fastify.addHook('onError', async (_request, _reply, error) => {
captureException(error);
function (fastify: FastifyWithHooks, _options: unknown, done: () => void): void {
fastify.addHook('onError', async (request, reply, error) => {
if (shouldHandleError(error, request, reply)) {
captureException(error);
}
});

// registering `onRequest` hook here instead of using Otel `onRequest` callback b/c `onRequest` hook
// is ironically called in the fastify `preHandler` hook which is called later in the lifecycle:
// https://fastify.dev/docs/latest/Reference/Lifecycle/
fastify.addHook('onRequest', async (request, _reply) => {
const reqWithRouteInfo = request as FastifyRequestRouteInfo;

// Taken from Otel Fastify instrumentation:
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-fastify/src/instrumentation.ts#L94-L96
const routeName = reqWithRouteInfo.routeOptions?.url || reqWithRouteInfo.routerPath;
const method = reqWithRouteInfo.method || 'GET';
const routeName = request.routeOptions?.url || request.routerPath;
const method = request.method || 'GET';

getIsolationScope().setTransactionName(`${method} ${routeName}`);
});
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(node): Add fastify `shouldHandleError` by AbhiPrasad · Pull Request #15771 · getsentry/sentry-javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions dev-packages/e2e-tests/test-applications/node-fastify/src/app.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -81,6 +81,16 @@ app.get('/test-error', async function (req, res) {
res.send({ exceptionId });
});

app.get('/test-4xx-error', async function (req, res) {
res.code(400);
throw new Error('This is a 4xx error');
});

app.get('/test-5xx-error', async function (req, res) {
res.code(500);
throw new Error('This is a 5xx error');
});

app.get<{ Params: { id: string } }>('/test-exception/:id', async function (req, res) {
throw new Error(`This is an exception with id ${req.params.id}`);
});
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,3 +28,25 @@ test('Sends correct error event', async ({ baseURL }) => {
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
});
});

test('Does not send 4xx errors by default', async ({ baseURL }) => {
// Define our test approach: we'll send both a 5xx and a 4xx request
// We should only see the 5xx error captured due to shouldHandleError's default behavior

// Create a promise to wait for the 500 error
const serverErrorPromise = waitForError('node-fastify', event => {
// Looking for a 500 error that should be captured
return !!event.exception?.values?.[0]?.value?.includes('This is a 5xx error');
});

// Make a request that will trigger a 400 error
const notFoundResponse = await fetch(`${baseURL}/test-4xx-error`);
expect(notFoundResponse.status).toBe(400);

// Make a request that will trigger a 500 error
await fetch(`${baseURL}/test-5xx-error`);

// Verify we receive the 500 error
const errorEvent = await serverErrorPromise;
expect(errorEvent.exception?.values?.[0]?.value).toContain('This is a 5xx error');
});
103 changes: 86 additions & 17 deletions packages/node/src/integrations/tracing/fastify.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,19 +12,14 @@ import type { IntegrationFn, Span } from '@sentry/core';
import { generateInstrumentOnce } from '../../otel/instrument';
import { ensureIsWrapped } from '../../utils/ensureIsWrapped';

// We inline the types we care about here
interface Fastify {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
register: (plugin: any) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
addHook: (hook: string, handler: (request: any, reply: any, error: Error) => void) => void;
}

/**
* Minimal request type containing properties around route information.
* Works for Fastify 3, 4 and presumably 5.
*
* Based on https://github.com/fastify/fastify/blob/ce3811f5f718be278bbcd4392c615d64230065a6/types/request.d.ts
*/
interface FastifyRequestRouteInfo {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
interface MinimalFastifyRequest extends Record<string, any> {
method?: string;
// since fastify@4.10.0
routeOptions?: {
Expand All@@ -33,6 +28,66 @@ interface FastifyRequestRouteInfo {
routerPath?: string;
}

/**
* Minimal reply type containing properties needed for error handling.
*
* Based on https://github.com/fastify/fastify/blob/ce3811f5f718be278bbcd4392c615d64230065a6/types/reply.d.ts
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
interface MinimalFastifyReply extends Record<string, any> {
statusCode: number;
}

// We inline the types we care about here
interface Fastify {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
register: (plugin: any) => void;
addHook: (hook: string, handler: (...params: unknown[]) => void) => void;
}

interface FastifyWithHooks extends Omit<Fastify, 'addHook'> {
addHook(
hook: 'onError',
handler: (request: MinimalFastifyRequest, reply: MinimalFastifyReply, error: Error) => void,
): void;
addHook(hook: 'onRequest', handler: (request: MinimalFastifyRequest, reply: MinimalFastifyReply) => void): void;
}

interface FastifyHandlerOptions {
/**
* Callback method deciding whether error should be captured and sent to Sentry
*
* @param error Captured Fastify error
* @param request Fastify request (or any object containing at least method, routeOptions.url, and routerPath)
* @param reply Fastify reply (or any object containing at least statusCode)
*
* @example
*
* ```javascript
* setupFastifyErrorHandler(app, {
* shouldHandleError(_error, _request, reply) {
* return reply.statusCode >= 400;
* },
* });
* ```
*
* If using TypeScript, you can cast the request and reply to get full type safety.
*
* ```typescript
* import type { FastifyRequest, FastifyReply } from 'fastify';
*
* setupFastifyErrorHandler(app, {
* shouldHandleError(error, minimalRequest, minimalReply) {
* const request = minimalRequest as FastifyRequest;
* const reply = minimalReply as FastifyReply;
* return reply.statusCode >= 500;
* },
* });
* ```
*/
shouldHandleError: (error: Error, request: MinimalFastifyRequest, reply: MinimalFastifyReply) => boolean;
}

const INTEGRATION_NAME = 'Fastify';

export const instrumentFastify = generateInstrumentOnce(
Expand DownExpand Up@@ -73,10 +128,22 @@ const _fastifyIntegration = (() => {
*/
export const fastifyIntegration = defineIntegration(_fastifyIntegration);

/**
* Default function to determine if an error should be sent to Sentry
*
* 3xx and 4xx errors are not sent by default.
*/
function defaultShouldHandleError(_error: Error, _request: MinimalFastifyRequest, reply: MinimalFastifyReply): boolean {
const statusCode = reply.statusCode;
// 3xx and 4xx errors are not sent by default.
return statusCode >= 500 || statusCode <= 299;
}

/**
* Add an Fastify error handler to capture errors to Sentry.
*
* @param fastify The Fastify instance to which to add the error handler
* @param options Configuration options for the handler
*
* @example
* ```javascript
Expand All@@ -92,23 +159,25 @@ export const fastifyIntegration = defineIntegration(_fastifyIntegration);
* app.listen({ port: 3000 });
* ```
*/
export function setupFastifyErrorHandler(fastify: Fastify): void {
export function setupFastifyErrorHandler(fastify: Fastify, options?: Partial<FastifyHandlerOptions>): void {
const shouldHandleError = options?.shouldHandleError || defaultShouldHandleError;

const plugin = Object.assign(
function (fastify: Fastify, _options: unknown, done: () => void): void {
fastify.addHook('onError', async (_request, _reply, error) => {
captureException(error);
function (fastify: FastifyWithHooks, _options: unknown, done: () => void): void {
fastify.addHook('onError', async (request, reply, error) => {
if (shouldHandleError(error, request, reply)) {
captureException(error);
}
});

// registering `onRequest` hook here instead of using Otel `onRequest` callback b/c `onRequest` hook
// is ironically called in the fastify `preHandler` hook which is called later in the lifecycle:
// https://fastify.dev/docs/latest/Reference/Lifecycle/
fastify.addHook('onRequest', async (request, _reply) => {
const reqWithRouteInfo = request as FastifyRequestRouteInfo;

// Taken from Otel Fastify instrumentation:
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-fastify/src/instrumentation.ts#L94-L96
const routeName = reqWithRouteInfo.routeOptions?.url || reqWithRouteInfo.routerPath;
const method = reqWithRouteInfo.method || 'GET';
const routeName = request.routeOptions?.url || request.routerPath;
const method = request.method || 'GET';

getIsolationScope().setTransactionName(`${method} ${routeName}`);
});
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); feat(node): Add fastify `shouldHandleError` by AbhiPrasad · Pull Request #15771 · getsentry/sentry-javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions dev-packages/e2e-tests/test-applications/node-fastify/src/app.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -81,6 +81,16 @@ app.get('/test-error', async function (req, res) {
res.send({ exceptionId });
});

app.get('/test-4xx-error', async function (req, res) {
res.code(400);
throw new Error('This is a 4xx error');
});

app.get('/test-5xx-error', async function (req, res) {
res.code(500);
throw new Error('This is a 5xx error');
});

app.get<{ Params: { id: string } }>('/test-exception/:id', async function (req, res) {
throw new Error(`This is an exception with id ${req.params.id}`);
});
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,3 +28,25 @@ test('Sends correct error event', async ({ baseURL }) => {
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
});
});

test('Does not send 4xx errors by default', async ({ baseURL }) => {
// Define our test approach: we'll send both a 5xx and a 4xx request
// We should only see the 5xx error captured due to shouldHandleError's default behavior

// Create a promise to wait for the 500 error
const serverErrorPromise = waitForError('node-fastify', event => {
// Looking for a 500 error that should be captured
return !!event.exception?.values?.[0]?.value?.includes('This is a 5xx error');
});

// Make a request that will trigger a 400 error
const notFoundResponse = await fetch(`${baseURL}/test-4xx-error`);
expect(notFoundResponse.status).toBe(400);

// Make a request that will trigger a 500 error
await fetch(`${baseURL}/test-5xx-error`);

// Verify we receive the 500 error
const errorEvent = await serverErrorPromise;
expect(errorEvent.exception?.values?.[0]?.value).toContain('This is a 5xx error');
});
103 changes: 86 additions & 17 deletions packages/node/src/integrations/tracing/fastify.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,19 +12,14 @@ import type { IntegrationFn, Span } from '@sentry/core';
import { generateInstrumentOnce } from '../../otel/instrument';
import { ensureIsWrapped } from '../../utils/ensureIsWrapped';

// We inline the types we care about here
interface Fastify {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
register: (plugin: any) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
addHook: (hook: string, handler: (request: any, reply: any, error: Error) => void) => void;
}

/**
* Minimal request type containing properties around route information.
* Works for Fastify 3, 4 and presumably 5.
*
* Based on https://github.com/fastify/fastify/blob/ce3811f5f718be278bbcd4392c615d64230065a6/types/request.d.ts
*/
interface FastifyRequestRouteInfo {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
interface MinimalFastifyRequest extends Record<string, any> {
method?: string;
// since fastify@4.10.0
routeOptions?: {
Expand All@@ -33,6 +28,66 @@ interface FastifyRequestRouteInfo {
routerPath?: string;
}

/**
* Minimal reply type containing properties needed for error handling.
*
* Based on https://github.com/fastify/fastify/blob/ce3811f5f718be278bbcd4392c615d64230065a6/types/reply.d.ts
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
interface MinimalFastifyReply extends Record<string, any> {
statusCode: number;
}

// We inline the types we care about here
interface Fastify {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
register: (plugin: any) => void;
addHook: (hook: string, handler: (...params: unknown[]) => void) => void;
}

interface FastifyWithHooks extends Omit<Fastify, 'addHook'> {
addHook(
hook: 'onError',
handler: (request: MinimalFastifyRequest, reply: MinimalFastifyReply, error: Error) => void,
): void;
addHook(hook: 'onRequest', handler: (request: MinimalFastifyRequest, reply: MinimalFastifyReply) => void): void;
}

interface FastifyHandlerOptions {
/**
* Callback method deciding whether error should be captured and sent to Sentry
*
* @param error Captured Fastify error
* @param request Fastify request (or any object containing at least method, routeOptions.url, and routerPath)
* @param reply Fastify reply (or any object containing at least statusCode)
*
* @example
*
* ```javascript
* setupFastifyErrorHandler(app, {
* shouldHandleError(_error, _request, reply) {
* return reply.statusCode >= 400;
* },
* });
* ```
*
* If using TypeScript, you can cast the request and reply to get full type safety.
*
* ```typescript
* import type { FastifyRequest, FastifyReply } from 'fastify';
*
* setupFastifyErrorHandler(app, {
* shouldHandleError(error, minimalRequest, minimalReply) {
* const request = minimalRequest as FastifyRequest;
* const reply = minimalReply as FastifyReply;
* return reply.statusCode >= 500;
* },
* });
* ```
*/
shouldHandleError: (error: Error, request: MinimalFastifyRequest, reply: MinimalFastifyReply) => boolean;
}

const INTEGRATION_NAME = 'Fastify';

export const instrumentFastify = generateInstrumentOnce(
Expand DownExpand Up@@ -73,10 +128,22 @@ const _fastifyIntegration = (() => {
*/
export const fastifyIntegration = defineIntegration(_fastifyIntegration);

/**
* Default function to determine if an error should be sent to Sentry
*
* 3xx and 4xx errors are not sent by default.
*/
function defaultShouldHandleError(_error: Error, _request: MinimalFastifyRequest, reply: MinimalFastifyReply): boolean {
const statusCode = reply.statusCode;
// 3xx and 4xx errors are not sent by default.
return statusCode >= 500 || statusCode <= 299;
}

/**
* Add an Fastify error handler to capture errors to Sentry.
*
* @param fastify The Fastify instance to which to add the error handler
* @param options Configuration options for the handler
*
* @example
* ```javascript
Expand All@@ -92,23 +159,25 @@ export const fastifyIntegration = defineIntegration(_fastifyIntegration);
* app.listen({ port: 3000 });
* ```
*/
export function setupFastifyErrorHandler(fastify: Fastify): void {
export function setupFastifyErrorHandler(fastify: Fastify, options?: Partial<FastifyHandlerOptions>): void {
const shouldHandleError = options?.shouldHandleError || defaultShouldHandleError;

const plugin = Object.assign(
function (fastify: Fastify, _options: unknown, done: () => void): void {
fastify.addHook('onError', async (_request, _reply, error) => {
captureException(error);
function (fastify: FastifyWithHooks, _options: unknown, done: () => void): void {
fastify.addHook('onError', async (request, reply, error) => {
if (shouldHandleError(error, request, reply)) {
captureException(error);
}
});

// registering `onRequest` hook here instead of using Otel `onRequest` callback b/c `onRequest` hook
// is ironically called in the fastify `preHandler` hook which is called later in the lifecycle:
// https://fastify.dev/docs/latest/Reference/Lifecycle/
fastify.addHook('onRequest', async (request, _reply) => {
const reqWithRouteInfo = request as FastifyRequestRouteInfo;

// Taken from Otel Fastify instrumentation:
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-fastify/src/instrumentation.ts#L94-L96
const routeName = reqWithRouteInfo.routeOptions?.url || reqWithRouteInfo.routerPath;
const method = reqWithRouteInfo.method || 'GET';
const routeName = request.routeOptions?.url || request.routerPath;
const method = request.method || 'GET';

getIsolationScope().setTransactionName(`${method} ${routeName}`);
});
Expand Down