Skip to content

feat(nitro-utils): Add patchEventHandler helper for nitro #14614

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 6, 2024
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
2 changes: 2 additions & 0 deletions packages/nitro-utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export { patchEventHandler } from './nitro/patchEventHandler';

export {
wrapServerEntryWithDynamicImport,
type WrapServerEntryPluginOptions,
Expand Down
39 changes: 39 additions & 0 deletions packages/nitro-utils/src/nitro/patchEventHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { getDefaultIsolationScope, getIsolationScope, logger, withIsolationScope } from '@sentry/core';
import type { EventHandler } from 'h3';
import { flushIfServerless } from '../util/flush';

/**
* A helper to patch a given `h3` event handler, ensuring that
* requests are properly isolated and data is flushed to Sentry.
*/
export function patchEventHandler(handler: EventHandler): EventHandler {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

L: We could give this a name that speaks more for itself (with the downside that it gets long). E.g. forkScopeAndFlushServerless

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I opted for a shorter name because where it's imported from gives enough context. But happy to change if you think otherwise, let me know :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No strong feelings about this :D

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will keep it then, can always change later since this isn't really public api.

return new Proxy(handler, {
async apply(handlerTarget, handlerThisArg, handlerArgs: Parameters<EventHandler>) {
// In environments where we cannot make use of the OTel
// http instrumentation (e.g. when using top level import
// of the server instrumentation file instead of
// `--import` or dynamic import, like on vercel)
// we still need to ensure requests are properly isolated
// by comparing the current isolation scope to the default
// one.
// Requests are properly isolated if they differ.
// If that's not the case, we fork the isolation scope here.
const isolationScope = getIsolationScope();
const newIsolationScope = isolationScope === getDefaultIsolationScope() ? isolationScope.clone() : isolationScope;

logger.log(
`Patched h3 event handler. ${
isolationScope === newIsolationScope ? 'Using existing' : 'Created new'
} isolation scope.`,
);

return withIsolationScope(newIsolationScope, async () => {
try {
return await handlerTarget.apply(handlerThisArg, handlerArgs);
} finally {
await flushIfServerless();
}
});
},
});
}
30 changes: 30 additions & 0 deletions packages/nitro-utils/src/util/flush.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { GLOBAL_OBJ, flush, getClient, logger, vercelWaitUntil } from '@sentry/core';

/**
* Flushes Sentry for serverless environments.
*/
export async function flushIfServerless(): Promise<void> {
const isServerless = !!process.env.LAMBDA_TASK_ROOT || !!process.env.VERCEL || !!process.env.NETLIFY;

// @ts-expect-error - this is not typed
if (GLOBAL_OBJ[Symbol.for('@vercel/request-context')]) {
vercelWaitUntil(flushWithTimeout());
} else if (isServerless) {
await flushWithTimeout();
}
}

/**
* Flushes Sentry.
*/
export async function flushWithTimeout(): Promise<void> {
const isDebug = getClient()?.getOptions()?.debug;

try {
isDebug && logger.log('Flushing events...');
await flush(2000);
isDebug && logger.log('Done flushing events');
} catch (e) {
isDebug && logger.log('Error while flushing events:\n', e);
}
}
Loading