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
Expand Up @@ -19,31 +19,6 @@ Sentry.init({
resMethod: res.req.method,
});
},
instrumentation: {
requestHook: (span, req) => {
span.setAttribute('attr1', 'yes');
Sentry.setExtra('requestHookCalled', {
url: req.url,
method: req.method,
});
},
responseHook: (span, res) => {
span.setAttribute('attr2', 'yes');
Sentry.setExtra('responseHookCalled', {
url: res.req.url,
method: res.req.method,
});
},
applyCustomAttributesOnSpan: (span, req, res) => {
span.setAttribute('attr3', 'yes');
Sentry.setExtra('applyCustomAttributesOnSpanCalled', {
reqUrl: req.url,
reqMethod: req.method,
resUrl: res.req.url,
resMethod: res.req.method,
});
},
},
}),
],
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,48 +22,6 @@ describe('httpIntegration', () => {

describe('instrumentation options', () => {
createEsmAndCjsTests(__dirname, 'server.mjs', 'instrument-options.mjs', (createRunner, test) => {
test('allows to pass instrumentation options to integration', async () => {
const runner = createRunner()
.expect({
transaction: {
contexts: {
trace: {
span_id: expect.stringMatching(/[a-f\d]{16}/),
trace_id: expect.stringMatching(/[a-f\d]{32}/),
data: {
'url.full': expect.stringMatching(/\/test$/),
'http.response.status_code': 200,
attr1: 'yes',
attr2: 'yes',
attr3: 'yes',
},
op: 'http.server',
status: 'ok',
},
},
extra: {
requestHookCalled: {
url: expect.stringMatching(/\/test$/),
method: 'GET',
},
responseHookCalled: {
url: expect.stringMatching(/\/test$/),
method: 'GET',
},
applyCustomAttributesOnSpanCalled: {
reqUrl: expect.stringMatching(/\/test$/),
reqMethod: 'GET',
resUrl: expect.stringMatching(/\/test$/),
resMethod: 'GET',
},
},
},
})
.start();
runner.makeRequest('get', '/test');
await runner.completed();
});

test('allows to configure incomingRequestSpanHook', async () => {
const runner = createRunner()
.expect({
Expand Down
42 changes: 42 additions & 0 deletions docs/migration/v11-end-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,47 @@ Two consequences to be aware of when upgrading:
- **Issue grouping:** Grouping in Sentry differs for events with and without stack traces, so you may see new issue groups after upgrading.
- **Release health:** Events with a stack trace are counted as errors, so a `captureMessage` call (including messages emitted by `captureConsoleIntegration`) now marks the current session as _errored_. This affects errored-session counts but does **not** mark sessions as crashed, so crash-free session rate is unaffected. If you use `captureMessage` for purely informational output, consider using Sentry Logs instead, which is better suited and does not affect release health.

### Incoming HTTP span hooks moved to `incomingRequestSpanHook`

Affected SDKs: `@sentry/node` and dependents.

The deprecated `httpIntegration` / `httpServerSpansIntegration` hooks `instrumentation.requestHook`, `instrumentation.responseHook`, and `instrumentation.applyCustomAttributesOnSpan` no longer run for incoming request spans. Use `incomingRequestSpanHook` (on `httpIntegration`) or `onSpanCreated` (on `httpServerSpansIntegration`) instead:

```js
// before
Sentry.httpIntegration({
instrumentation: {
requestHook: (span, req) => {
span.setAttribute('custom', true);
},
},
});

// after
Sentry.httpIntegration({
incomingRequestSpanHook: (span, req, res) => {
span.setAttribute('custom', true);
},
});
```

`httpIntegration`'s `instrumentation` option is still honored for **outgoing** request spans.

### Node HTTP transport `keepAlive` defaults to `true`

Affected SDKs: `@sentry/node` and dependents.

The Node HTTP transport now reuses sockets by default (`keepAlive: true`). The previous default of `false` existed because of a memory leak in Node 8, which is no longer relevant (minimum Node is 20.19.0). Idle sockets are still closed after 2 seconds. Pass `keepAlive: false` in transport options to restore the previous behavior:

```js
Sentry.init({
dsn: '__DSN__',
transportOptions: {
keepAlive: false,
},
});
```

### `tracePropagationTargets` matching is now case-insensitive

Affected SDKs: All SDKs.
Expand Down Expand Up @@ -796,6 +837,7 @@ Sentry.init({
- (Express) The deprecated `patchExpressModule(options)` signature was removed. Use `patchExpressModule(moduleExports, getOptions)` instead.
- The `@sentry/node-core/light/otlp` entry point was removed, along with its optional `@opentelemetry/exporter-trace-otlp-http` peer dependency. `otlpIntegration` is now exported directly from every server-side SDK, so `Sentry.otlpIntegration()` needs no extra import or install.
- The `otlpIntegration` options `setupOtlpTracesExporter` and `collectorUrl` were removed, and the integration no longer sets up a span exporter, span processor, or tracer provider. Configure your own exporter and point it at `Sentry.getOtlpTracesEndpoint(dsn)`, or at your collector's URL if you route through one. See [Connecting Sentry to your OpenTelemetry traces](#connecting-sentry-to-your-opentelemetry-traces).
- The deprecated `httpServerSpansIntegration` `instrumentation.{requestHook,responseHook,applyCustomAttributesOnSpan}` option was removed. Use `onSpanCreated`, or `httpIntegration({ incomingRequestSpanHook })`, to mutate incoming request spans.

### `@sentry/cloudflare`

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { ChannelListener } from 'node:diagnostics_channel';
import { subscribe, unsubscribe } from 'node:diagnostics_channel';
import { subscribe } from 'node:diagnostics_channel';
import { context, trace } from '@opentelemetry/api';
import type { ClientRequest, IncomingMessage, ServerResponse } from 'node:http';
import type { HttpClientRequest, HttpIncomingMessage, HttpInstrumentationOptions, Span } from '@sentry/core';
Expand Down Expand Up @@ -137,7 +136,6 @@ export type SentryHttpInstrumentationOptions = OutgoingHttpRequestInstrumentatio
* It uses the diagnostics channel if available, otherwise it falls back to monkey-patching.
*
* The instrumentation will start spans, create breadcrumbs, and propagate trace headers in outgoing requests (depending on the settings).
* This can be called multiple times, where the last invocation will have the current options.
*
* @TODO Cleanup options in v11
*/
Expand Down Expand Up @@ -195,17 +193,9 @@ export function instrumentHttpOutgoingRequests(
}
}

let _currentListener: ChannelListener | undefined;
function instrumentHttpOutgoingRequestsViaChannel(options: HttpInstrumentationOptions): void {
const { [HTTP_ON_CLIENT_REQUEST]: onHttpClientRequestCreated } = getHttpClientSubscriptions(options);
// If it was previously subscribed, first unsubscribe it
// TODO(v11): We can likely remove this when we drop preload support
if (_currentListener) {
unsubscribe(HTTP_ON_CLIENT_REQUEST, _currentListener);
}

subscribe(HTTP_ON_CLIENT_REQUEST, onHttpClientRequestCreated);
_currentListener = onHttpClientRequestCreated;
}
Comment thread
RulaKhaled marked this conversation as resolved.

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import {
} from '@sentry/conventions/attributes';
import type {
Event,
HttpClientRequest,
HttpIncomingMessage,
HttpServerResponse,
Integration,
Expand Down Expand Up @@ -93,19 +92,6 @@ export interface HttpServerSpansIntegrationOptions {
*/
ignoreStatusCodes?: (number | [number, number])[];

/**
* @deprecated This is deprecated in favor of `incomingRequestSpanHook`.
*/
instrumentation?: {
requestHook?: (span: Span, req: HttpClientRequest | HttpIncomingMessage) => void;
responseHook?: (span: Span, response: HttpIncomingMessage | HttpServerResponse) => void;
applyCustomAttributesOnSpan?: (
span: Span,
request: HttpClientRequest | HttpIncomingMessage,
response: HttpIncomingMessage | HttpServerResponse,
) => void;
};

/**
* A hook that can be used to mutate the span for incoming requests.
* This is triggered after the span is created, but before it is recorded.
Expand All @@ -124,8 +110,6 @@ const _httpServerSpansIntegration = ((options: HttpServerSpansIntegrationOptions
];

const { onSpanCreated } = options;
// eslint-disable-next-line typescript/no-deprecated
const { requestHook, responseHook, applyCustomAttributesOnSpan } = options.instrumentation ?? {};

return {
name: INTEGRATION_NAME,
Expand Down Expand Up @@ -203,10 +187,6 @@ const _httpServerSpansIntegration = ((options: HttpServerSpansIntegrationOptions
},
});

// TODO v11: Remove the following three hooks, only onSpanCreated should remain
requestHook?.(span, request);
responseHook?.(span, response);
applyCustomAttributesOnSpan?.(span, request, response);
onSpanCreated?.(span, request, response);

return withActiveSpan(span, () => {
Expand Down
5 changes: 2 additions & 3 deletions packages/node/src/integrations/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ interface HttpOptions {
disableIncomingRequestSpans?: boolean;

/**
* Additional instrumentation options that are passed to the underlying HttpInstrumentation.
* Hooks for outgoing HTTP request spans.
* These no longer run for incoming request spans; use `incomingRequestSpanHook` for those.
*/
instrumentation?: {
requestHook?: (span: Span, req: HttpIncomingMessage | HttpClientRequest) => void;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

low/cleanup: These are now only for outgoing requests now, but the types still accept HttpClientRequest for the request hook and HttpIncomingMessage for the response hook. The types can probably be cleaned up, since it's more restrictive now.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Already handled in the stacked PR

Expand Down Expand Up @@ -174,8 +175,6 @@ export const httpIntegration = defineIntegration((options: HttpOptions = {}) =>
ignoreIncomingRequests: options.ignoreIncomingRequests,
ignoreStaticAssets: options.ignoreStaticAssets,
ignoreStatusCodes: options.dropSpansForIncomingRequestStatusCodes,
// oxlint-disable-next-line typescript/no-deprecated -- pass through the deprecated option for back-compat
instrumentation: options.instrumentation,
onSpanCreated: options.incomingRequestSpanHook,
};

Expand Down
133 changes: 71 additions & 62 deletions packages/node/src/transports/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface NodeTransportOptions extends BaseTransportOptions {
caCerts?: string | Buffer | Array<string | Buffer>;
/** Custom HTTP module. Defaults to the native 'http' and 'https' modules. */
httpModule?: HTTPModule;
/** Allow overriding connection keepAlive, defaults to false */
/** Allow overriding connection keepAlive, defaults to true */
keepAlive?: boolean;
}

Expand Down Expand Up @@ -68,10 +68,7 @@ export function makeNodeTransport(options: NodeTransportOptions): Transport {
);

const nativeHttpModule = isHttps ? https : http;
const keepAlive = options.keepAlive === undefined ? false : options.keepAlive;

// TODO(v11): Evaluate if we can set keepAlive to true. This would involve testing for memory leaks in older node
// versions(>= 8) as they had memory leaks when using it: #2555
const keepAlive = options.keepAlive ?? true;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The node 8 memory leaks are not an issue, but I think this might have some further reaching consequences, since this transport is also used for aws-serverless and google-cloud-serverless use, so it's possible that the socket times out while the process is frozen. (Also, this is ignored for the proxy case.)

It's not going to fail often, but I think we'd need to add a retry in makeRequest to make sure that if we get a dead socket, we don't crash on it.

Eg, wrap the contents of makeRequest in an async sendRequest = (canRetry: boolean) => {...}, and then have it do:

req.on('error', error => {
  if (canRetry && req.reusedSocket && (error as { code?: string }).code === 'ECONNRESET') {
    resolve(sendRequest(false));
  } else {
    reject(error);
  }
});

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Nice catch! will add the keepAlive reused-socket retry

const agent = proxy
? (new HttpsProxyAgent(proxy) as http.Agent)
: new nativeHttpModule.Agent({ keepAlive, maxSockets: 30, timeout: 2000 });
Expand Down Expand Up @@ -113,63 +110,75 @@ function createRequestExecutor(
): TransportRequestExecutor {
const { hostname, pathname, port, protocol, search } = new URL(options.url);
return function makeRequest(request: TransportRequest): Promise<TransportMakeRequestResponse> {
return new Promise((resolve, reject) => {
// This ensures we do not generate any spans in OpenTelemetry for the transport
suppressTracing(() => {
let body = streamFromBody(request.body);

const headers: Record<string, string> = { ...options.headers };

if (request.body.length > GZIP_THRESHOLD) {
headers['content-encoding'] = 'gzip';
body = body.pipe(createGzip());
}

const hostnameIsIPv6 = hostname.startsWith('[');

const req = httpModule.request(
{
method: 'POST',
agent,
headers,
// Remove "[" and "]" from IPv6 hostnames
hostname: hostnameIsIPv6 ? hostname.slice(1, -1) : hostname,
path: `${pathname}${search}`,
port,
protocol,
ca: options.caCerts,
},
res => {
res.on('data', () => {
// Drain socket
});

res.on('end', () => {
// Drain socket
});

res.setEncoding('utf8');

// "Key-value pairs of header names and values. Header names are lower-cased."
// https://nodejs.org/api/http.html#http_message_headers
const retryAfterHeader = res.headers['retry-after'] ?? null;
const rateLimitsHeader = res.headers['x-sentry-rate-limits'] ?? null;

resolve({
statusCode: res.statusCode,
headers: {
'retry-after': retryAfterHeader,
'x-sentry-rate-limits': Array.isArray(rateLimitsHeader)
? rateLimitsHeader[0] || null
: rateLimitsHeader,
},
});
},
);

req.on('error', reject);
body.pipe(req);
const sendRequest = (canRetry: boolean): Promise<TransportMakeRequestResponse> =>
new Promise((resolve, reject) => {
// This ensures we do not generate any spans in OpenTelemetry for the transport
suppressTracing(() => {
// Recreate the body on each attempt so a retry is not piping a consumed stream.
let body = streamFromBody(request.body);

const headers: Record<string, string> = { ...options.headers };

if (request.body.length > GZIP_THRESHOLD) {
headers['content-encoding'] = 'gzip';
body = body.pipe(createGzip());
}

const hostnameIsIPv6 = hostname.startsWith('[');

const req = httpModule.request(
{
method: 'POST',
agent,
headers,
// Remove "[" and "]" from IPv6 hostnames
hostname: hostnameIsIPv6 ? hostname.slice(1, -1) : hostname,
path: `${pathname}${search}`,
port,
protocol,
ca: options.caCerts,
},
res => {
res.on('data', () => {
// Drain socket
});

res.on('end', () => {
// Drain socket
});

res.setEncoding('utf8');

// "Key-value pairs of header names and values. Header names are lower-cased."
// https://nodejs.org/api/http.html#http_message_headers
const retryAfterHeader = res.headers['retry-after'] ?? null;
const rateLimitsHeader = res.headers['x-sentry-rate-limits'] ?? null;

resolve({
statusCode: res.statusCode,
headers: {
'retry-after': retryAfterHeader,
'x-sentry-rate-limits': Array.isArray(rateLimitsHeader)
? rateLimitsHeader[0] || null
: rateLimitsHeader,
},
});
},
);

req.on('error', error => {
// Keep-alive sockets can go stale while a serverless isolate is frozen.
// Node recommends a single retry when the reused socket resets.
if (canRetry && req.reusedSocket && (error as NodeJS.ErrnoException).code === 'ECONNRESET') {
resolve(sendRequest(false));
} else {
reject(error);
}
});
body.pipe(req);
});
});
});

return sendRequest(true);
};
}
Loading
Loading