diff --git a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-forward-request-hook/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-forward-request-hook/scenario.ts new file mode 100644 index 000000000000..0843830321c4 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-forward-request-hook/scenario.ts @@ -0,0 +1,26 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, + integrations: [ + Sentry.nativeNodeFetchIntegration({ + requestHook: (span, req) => { + span.setAttribute('sentry.request.hook', req.path); + }, + responseHook: (span, { response, request }) => { + span.setAttribute('sentry.response.hook.path', request.path); + span.setAttribute('sentry.response.hook.status_code', response.statusCode); + }, + }), + ], +}); + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +Sentry.startSpan({ name: 'test_transaction' }, async () => { + await fetch(`${process.env.SERVER_URL}/api/v0`); + await fetch(`${process.env.SERVER_URL}/api/v1`); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-forward-request-hook/test.ts b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-forward-request-hook/test.ts new file mode 100644 index 000000000000..8d0a35a43d05 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-forward-request-hook/test.ts @@ -0,0 +1,58 @@ +import { expect, test } from 'vitest'; +import { createRunner } from '../../../../utils/runner'; +import { createTestServer } from '../../../../utils/server'; + +test('adds requestHook and responseHook attributes to spans of outgoing fetch requests', async () => { + expect.assertions(3); + + const [SERVER_URL, closeTestServer] = await createTestServer() + .get('/api/v0', () => { + // Just ensure we're called + expect(true).toBe(true); + }) + .get( + '/api/v1', + () => { + // Just ensure we're called + expect(true).toBe(true); + }, + 404, + ) + .start(); + + await createRunner(__dirname, 'scenario.ts') + .withEnv({ SERVER_URL }) + .expect({ + transaction: { + transaction: 'test_transaction', + spans: [ + expect.objectContaining({ + description: expect.stringMatching(/GET .*\/api\/v0/), + op: 'http.client', + origin: 'auto.http.otel.node_fetch', + status: 'ok', + data: expect.objectContaining({ + 'sentry.request.hook': '/api/v0', + 'sentry.response.hook.path': '/api/v0', + 'sentry.response.hook.status_code': 200, + }), + }), + expect.objectContaining({ + description: expect.stringMatching(/GET .*\/api\/v1/), + op: 'http.client', + origin: 'auto.http.otel.node_fetch', + status: 'not_found', + data: expect.objectContaining({ + 'sentry.request.hook': '/api/v1', + 'sentry.response.hook.path': '/api/v1', + 'sentry.response.hook.status_code': 404, + 'http.response.status_code': 404, + }), + }), + ], + }, + }) + .start() + .completed(); + closeTestServer(); +}); diff --git a/packages/node/src/integrations/node-fetch.ts b/packages/node/src/integrations/node-fetch.ts index 437806e16dbc..6da9fd628bac 100644 --- a/packages/node/src/integrations/node-fetch.ts +++ b/packages/node/src/integrations/node-fetch.ts @@ -8,7 +8,7 @@ import type { NodeClientOptions } from '../types'; const INTEGRATION_NAME = 'NodeFetch'; -interface NodeFetchOptions { +interface NodeFetchOptions extends Pick { /** * Whether breadcrumbs should be recorded for requests. * Defaults to true @@ -106,6 +106,8 @@ function getConfigWithDefaults(options: Partial = {}): UndiciI [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.node_fetch', }; }, + requestHook: options.requestHook, + responseHook: options.responseHook, } satisfies UndiciInstrumentationConfig; return instrumentationConfig;