Skip to content

Commit

Permalink
feat(nuxt): Add Http responseHook with waitUntil (#13986)
Browse files Browse the repository at this point in the history
With
[waitUntil](https://vercel.com/docs/functions/functions-api-reference#waituntil)
the lambda execution continues until all async tasks (like sending data
to Sentry) are done.

Timing-wise it should work like this: `span.end()` -> `waitUntil()` ->
Nitro/Node `response.end()`

The problem in [this
PR](#13895) was that
the Nitro hook `afterResponse` is called to late (after
`response.end()`), so `waitUntil()` could not be added to this hook.

---

Just for reference how this is done in Nitro (and h3, the underlying
http framework):

1. The Nitro `afterResponse` hook is called in `onAfterResponse`

https:/unjs/nitro/blob/359af68d2b3d51d740cf869d0f13aec0c5e9f565/src/runtime/internal/app.ts#L71-L77

2. h3 `onAfterResponse` is called after the Node response was sent (and
`onBeforeResponse` is called too early for calling `waitUntil`, as the
span just starts at this point):

https:/unjs/h3/blob/7324eeec854eecc37422074ef9f2aec8a5e4a816/src/adapters/node/index.ts#L38-L47

- `endNodeResponse` calls `response.end()`:
https:/unjs/h3/blob/7324eeec854eecc37422074ef9f2aec8a5e4a816/src/adapters/node/internal/utils.ts#L58
  • Loading branch information
s1gr1d authored Oct 17, 2024
1 parent 6bc37f0 commit ff7a07d
Showing 1 changed file with 38 additions and 4 deletions.
42 changes: 38 additions & 4 deletions packages/nuxt/src/server/sdk.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { applySdkMetadata, getGlobalScope } from '@sentry/core';
import { init as initNode } from '@sentry/node';
import type { Client, EventProcessor } from '@sentry/types';
import { logger } from '@sentry/utils';
import { applySdkMetadata, flush, getGlobalScope } from '@sentry/core';
import {
type NodeOptions,
getDefaultIntegrations as getDefaultNodeIntegrations,
httpIntegration,
init as initNode,
} from '@sentry/node';
import type { Client, EventProcessor, Integration } from '@sentry/types';
import { logger, vercelWaitUntil } from '@sentry/utils';
import { DEBUG_BUILD } from '../common/debug-build';
import type { SentryNuxtServerOptions } from '../common/types';

Expand All @@ -14,6 +19,7 @@ export function init(options: SentryNuxtServerOptions): Client | undefined {
const sentryOptions = {
...options,
registerEsmLoaderHooks: mergeRegisterEsmLoaderHooks(options),
defaultIntegrations: getNuxtDefaultIntegrations(options),
};

applySdkMetadata(sentryOptions, 'nuxt', ['nuxt', 'node']);
Expand Down Expand Up @@ -46,6 +52,21 @@ export function init(options: SentryNuxtServerOptions): Client | undefined {
return client;
}

function getNuxtDefaultIntegrations(options: NodeOptions): Integration[] {
return [
...getDefaultNodeIntegrations(options).filter(integration => integration.name !== 'Http'),
// The httpIntegration is added as defaultIntegration, so users can still overwrite it
httpIntegration({
instrumentation: {
responseHook: () => {
// Makes it possible to end the tracing span before closing the Vercel lambda (https://vercel.com/docs/functions/functions-api-reference#waituntil)
vercelWaitUntil(flushSafelyWithTimeout());
},
},
}),
];
}

/**
* Adds /vue/ to the registerEsmLoaderHooks options and merges it with the old values in the array if one is defined.
* If the registerEsmLoaderHooks option is already a boolean, nothing is changed.
Expand All @@ -64,3 +85,16 @@ export function mergeRegisterEsmLoaderHooks(
}
return options.registerEsmLoaderHooks ?? { exclude: [/vue/] };
}

/**
* Flushes pending Sentry events with a 2-second timeout and in a way that cannot create unhandled promise rejections.
*/
export async function flushSafelyWithTimeout(): Promise<void> {
try {
DEBUG_BUILD && logger.log('Flushing events...');
await flush(2000);
DEBUG_BUILD && logger.log('Done flushing events');
} catch (e) {
DEBUG_BUILD && logger.log('Error while flushing events:\n', e);
}
}

0 comments on commit ff7a07d

Please sign in to comment.