Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { createServer } from 'http';
import { loggingTransport } from '@sentry-internal/node-integration-tests';
import * as Sentry from '@sentry/node';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
});

// Bind and immediately release a port so we have an address that reliably refuses the connection.
// A refused outgoing request fires the `undici:request:error` channel, exercising the error path.
function getRefusedPort(): Promise<number> {
return new Promise(resolve => {
const server = createServer();
server.listen(0, () => {
const { port } = server.address() as { port: number };
server.close(() => resolve(port));
});
});
}

async function run(): Promise<void> {
const port = await getRefusedPort();

await Sentry.startSpan({ name: 'test_transaction' }, async () => {
await fetch(`http://localhost:${port}/api/v0`).catch(() => {
// Ignore the expected connection error
});
});
}

// eslint-disable-next-line @typescript-eslint/no-floating-promises
run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { expect, test } from 'vitest';
import { createRunner } from '../../../../utils/runner';

test('captures an errored span for a failed outgoing fetch request', async () => {
await createRunner(__dirname, 'scenario.ts')
.expect({
transaction: {
transaction: 'test_transaction',
spans: expect.arrayContaining([
expect.objectContaining({
description: expect.stringMatching(/GET http:\/\/localhost:\d+\//),
op: 'http.client',
origin: 'auto.http.otel.node_fetch',
status: 'internal_error',
}),
]),
},
})
.start()
.completed();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { loggingTransport } from '@sentry-internal/node-integration-tests';
import * as Sentry from '@sentry/node';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
integrations: [
Sentry.nativeNodeFetchIntegration({
headersToSpanAttributes: {
requestHeaders: ['x-test-header'],
responseHeaders: ['x-powered-by'],
},
}),
],
});

// eslint-disable-next-line @typescript-eslint/no-floating-promises
Sentry.startSpan({ name: 'test_transaction' }, async () => {
await fetch(`${process.env.SERVER_URL}/api/v0`, { headers: { 'x-test-header': 'test-value' } });
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { createTestServer } from '@sentry-internal/test-utils';
import { expect, test } from 'vitest';
import { createRunner } from '../../../../utils/runner';

test('maps configured request & response headers to span attributes', async () => {
expect.assertions(2);

const [SERVER_URL, closeTestServer] = await createTestServer()
.get('/api/v0', headers => {
expect(headers['x-test-header']).toBe('test-value');
})
.start();

await createRunner(__dirname, 'scenario.ts')
.withEnv({ SERVER_URL })
.expect({
transaction: {
transaction: 'test_transaction',
spans: expect.arrayContaining([
expect.objectContaining({
description: expect.stringMatching(/GET .*\/api\/v0/),
op: 'http.client',
origin: 'auto.http.otel.node_fetch',
data: expect.objectContaining({
'http.request.header.x-test-header': ['test-value'],
'http.response.header.x-powered-by': ['Express'],
}),
}),
]),
},
})
.start()
.completed();
closeTestServer();
});
1 change: 0 additions & 1 deletion packages/node/src/integrations/node-fetch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ function _shouldInstrumentSpans(options: NodeFetchOptions, clientOptions: Partia
/** Exported only for tests. */
export function _getConfigWithDefaults(options: Partial<NodeFetchOptions> = {}): UndiciInstrumentationConfig {
const instrumentationConfig = {
requireParentforSpans: false,
ignoreRequestHook: request => {
const url = getAbsoluteUrl(request.origin, request.path);
const _ignoreOutgoingRequests = options.ignoreOutgoingRequests;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
* - Upstream version: @opentelemetry/instrumentation-undici@0.24.0
* - Tracking issue: https://github.com/getsentry/sentry-javascript/issues/20165
*/
/* eslint-disable -- vendored @opentelemetry/instrumentation-undici (#20165) */

import type { UndiciRequest, UndiciResponse } from './types';

Expand Down
23 changes: 23 additions & 0 deletions packages/node/src/integrations/node-fetch/vendored/semconv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*
* NOTICE from the Sentry authors:
* - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/ed97091c9890dd18e52759f2ea98e9d7593b3ae4/packages/instrumentation-undici
* - Upstream version: @opentelemetry/instrumentation-undici@0.24.0
* - The semantic-convention constants this package emits, inlined from
* `@opentelemetry/semantic-conventions` (matching the sibling vendored dirs).
*/

export const ATTR_HTTP_REQUEST_METHOD = 'http.request.method' as const;
export const ATTR_HTTP_REQUEST_METHOD_ORIGINAL = 'http.request.method_original' as const;
export const ATTR_HTTP_RESPONSE_STATUS_CODE = 'http.response.status_code' as const;
export const ATTR_NETWORK_PEER_ADDRESS = 'network.peer.address' as const;
export const ATTR_NETWORK_PEER_PORT = 'network.peer.port' as const;
export const ATTR_SERVER_ADDRESS = 'server.address' as const;
export const ATTR_SERVER_PORT = 'server.port' as const;
export const ATTR_URL_FULL = 'url.full' as const;
export const ATTR_URL_PATH = 'url.path' as const;
export const ATTR_URL_QUERY = 'url.query' as const;
export const ATTR_URL_SCHEME = 'url.scheme' as const;
export const ATTR_USER_AGENT_ORIGINAL = 'user_agent.original' as const;
7 changes: 2 additions & 5 deletions packages/node/src/integrations/node-fetch/vendored/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@
* - Upstream version: @opentelemetry/instrumentation-undici@0.24.0
* - Tracking issue: https://github.com/getsentry/sentry-javascript/issues/20165
*/
/* eslint-disable -- vendored @opentelemetry/instrumentation-undici (#20165) */

import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
import type { Attributes, Span } from '@opentelemetry/api';
import type { Span, SpanAttributes } from '@sentry/core';

export interface UndiciRequest {
origin: string;
Expand Down Expand Up @@ -54,7 +53,7 @@ export interface ResponseHookFunction<RequestType = UndiciRequest, ResponseType
}

export interface StartSpanHookFunction<T = UndiciRequest> {
(request: T): Attributes;
(request: T): SpanAttributes;
}

// This package will instrument HTTP requests made through `undici` or `fetch` global API
Expand All @@ -71,8 +70,6 @@ export interface UndiciInstrumentationConfig<
responseHook?: ResponseHookFunction<RequestType, ResponseType>;
/** Function for adding custom attributes before a span is started */
startSpanHook?: StartSpanHookFunction<RequestType>;
/** Require parent to create span for outgoing requests */
requireParentforSpans?: boolean;
/** Map the following HTTP headers to span attributes. */
headersToSpanAttributes?: {
requestHeaders?: string[];
Expand Down
Loading
Loading