From 0f88c6be3fa7b32cc5f76359f646f01326d86e17 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 23 Mar 2023 09:36:59 +0100 Subject: [PATCH 01/11] feat(node): Undici integration --- .gitignore | 1 + .../src/integrations/diagnostics_channel.d.ts | 172 ++++++++++++++++++ packages/node/src/integrations/undici.ts | 46 +++++ 3 files changed, 219 insertions(+) create mode 100644 packages/node/src/integrations/diagnostics_channel.d.ts create mode 100644 packages/node/src/integrations/undici.ts diff --git a/.gitignore b/.gitignore index 8574a81de0a4..ef9cf4ac2cc2 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ dist/ coverage/ scratch/ *.d.ts +!diagnostics_channel.d.ts *.js.map *.pyc *.tsbuildinfo diff --git a/packages/node/src/integrations/diagnostics_channel.d.ts b/packages/node/src/integrations/diagnostics_channel.d.ts new file mode 100644 index 000000000000..65cde75bb734 --- /dev/null +++ b/packages/node/src/integrations/diagnostics_channel.d.ts @@ -0,0 +1,172 @@ +// Vendored from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/8fc6d58e6434810867a9483e2107ea51bfca9153/types/node/diagnostics_channel.d.ts + +// License: +// This project is licensed under the MIT license. +// Copyrights are respective of each contributor listed at the beginning of each definition file. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files(the "Software"), to deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and / or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS +// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +// Vendored code starts here: +/** + * The `diagnostics_channel` module provides an API to create named channels + * to report arbitrary message data for diagnostics purposes. + * + * It can be accessed using: + * + * ```js + * import diagnostics_channel from 'diagnostics_channel'; + * ``` + * + * It is intended that a module writer wanting to report diagnostics messages + * will create one or many top-level channels to report messages through. + * Channels may also be acquired at runtime but it is not encouraged + * due to the additional overhead of doing so. Channels may be exported for + * convenience, but as long as the name is known it can be acquired anywhere. + * + * If you intend for your module to produce diagnostics data for others to + * consume it is recommended that you include documentation of what named + * channels are used along with the shape of the message data. Channel names + * should generally include the module name to avoid collisions with data from + * other modules. + * @experimental + * @see [source](https://github.com/nodejs/node/blob/v18.0.0/lib/diagnostics_channel.js) + */ +declare module 'diagnostics_channel' { + /** + * Check if there are active subscribers to the named channel. This is helpful if + * the message you want to send might be expensive to prepare. + * + * This API is optional but helpful when trying to publish messages from very + * performance-sensitive code. + * + * ```js + * import diagnostics_channel from 'diagnostics_channel'; + * + * if (diagnostics_channel.hasSubscribers('my-channel')) { + * // There are subscribers, prepare and publish message + * } + * ``` + * @since v15.1.0, v14.17.0 + * @param name The channel name + * @return If there are active subscribers + */ + function hasSubscribers(name: string | symbol): boolean; + /** + * This is the primary entry-point for anyone wanting to interact with a named + * channel. It produces a channel object which is optimized to reduce overhead at + * publish time as much as possible. + * + * ```js + * import diagnostics_channel from 'diagnostics_channel'; + * + * const channel = diagnostics_channel.channel('my-channel'); + * ``` + * @since v15.1.0, v14.17.0 + * @param name The channel name + * @return The named channel object + */ + function channel(name: string | symbol): Channel; + type ChannelListener = (message: unknown, name: string | symbol) => void; + /** + * The class `Channel` represents an individual named channel within the data + * pipeline. It is use to track subscribers and to publish messages when there + * are subscribers present. It exists as a separate object to avoid channel + * lookups at publish time, enabling very fast publish speeds and allowing + * for heavy use while incurring very minimal cost. Channels are created with {@link channel}, constructing a channel directly + * with `new Channel(name)` is not supported. + * @since v15.1.0, v14.17.0 + */ + class Channel { + public readonly name: string | symbol; + /** + * Check if there are active subscribers to this channel. This is helpful if + * the message you want to send might be expensive to prepare. + * + * This API is optional but helpful when trying to publish messages from very + * performance-sensitive code. + * + * ```js + * import diagnostics_channel from 'diagnostics_channel'; + * + * const channel = diagnostics_channel.channel('my-channel'); + * + * if (channel.hasSubscribers) { + * // There are subscribers, prepare and publish message + * } + * ``` + * @since v15.1.0, v14.17.0 + */ + public readonly hasSubscribers: boolean; + private constructor(name: string | symbol); + /** + * Publish a message to any subscribers to the channel. This will + * trigger message handlers synchronously so they will execute within + * the same context. + * + * ```js + * import diagnostics_channel from 'diagnostics_channel'; + * + * const channel = diagnostics_channel.channel('my-channel'); + * + * channel.publish({ + * some: 'message' + * }); + * ``` + * @since v15.1.0, v14.17.0 + * @param message The message to send to the channel subscribers + */ + public publish(message: unknown): void; + /** + * Register a message handler to subscribe to this channel. This message handler + * will be run synchronously whenever a message is published to the channel. Any + * errors thrown in the message handler will trigger an `'uncaughtException'`. + * + * ```js + * import diagnostics_channel from 'diagnostics_channel'; + * + * const channel = diagnostics_channel.channel('my-channel'); + * + * channel.subscribe((message, name) => { + * // Received data + * }); + * ``` + * @since v15.1.0, v14.17.0 + * @param onMessage The handler to receive channel messages + */ + public subscribe(onMessage: ChannelListener): void; + /** + * Remove a message handler previously registered to this channel with `channel.subscribe(onMessage)`. + * + * ```js + * import diagnostics_channel from 'diagnostics_channel'; + * + * const channel = diagnostics_channel.channel('my-channel'); + * + * function onMessage(message, name) { + * // Received data + * } + * + * channel.subscribe(onMessage); + * + * channel.unsubscribe(onMessage); + * ``` + * @since v15.1.0, v14.17.0 + * @param onMessage The previous subscribed handler to remove + * @return `true` if the handler was found, `false` otherwise. + */ + public unsubscribe(onMessage: ChannelListener): void; + } +} +declare module 'node:diagnostics_channel' { + export * from 'diagnostics_channel'; +} diff --git a/packages/node/src/integrations/undici.ts b/packages/node/src/integrations/undici.ts new file mode 100644 index 000000000000..56a72ae7f4be --- /dev/null +++ b/packages/node/src/integrations/undici.ts @@ -0,0 +1,46 @@ +import type { Hub } from '@sentry/core'; +import type { EventProcessor, Integration } from '@sentry/types'; +import type DiagnosticsChannel from 'diagnostics_channel'; + +/** */ +export class Undici implements Integration { + /** + * @inheritDoc + */ + public static id: string = 'Undici'; + + /** + * @inheritDoc + */ + public name: string = Undici.id; + + // Have to hold all built channels in memory otherwise they get garbage collected + // See: https://github.com/nodejs/node/pull/42714 + // This has been fixed in Node 19+ + private _channels: Map = new Map(); + + /** + * @inheritDoc + */ + public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void, _getCurrentHub: () => Hub): void { + let ds: typeof DiagnosticsChannel | undefined; + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + ds = require('diagnostics_channel') as typeof DiagnosticsChannel; + } catch (e) { + // no-op + } + + if (!ds) { + return; + } + + // https://github.com/nodejs/undici/blob/main/docs/api/DiagnosticsChannel.md + const undiciChannel = ds.channel('undici:request'); + } + + private _setupChannel(name: Parameters[0]): void { + const channel = DiagnosticsChannel.channel(name); + if (node) + } +} From f9f4e23b63eb5053641e43bbfdb24491cc33af19 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 23 Mar 2023 12:42:48 +0100 Subject: [PATCH 02/11] quite a bit more code --- .gitignore | 1 - packages/node/.gitignore | 1 + .../src/integrations/diagnostics_channel.d.ts | 15 ++ packages/node/src/integrations/undici.ts | 201 +++++++++++++++++- 4 files changed, 209 insertions(+), 9 deletions(-) create mode 100644 packages/node/.gitignore diff --git a/.gitignore b/.gitignore index ef9cf4ac2cc2..8574a81de0a4 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ dist/ coverage/ scratch/ *.d.ts -!diagnostics_channel.d.ts *.js.map *.pyc *.tsbuildinfo diff --git a/packages/node/.gitignore b/packages/node/.gitignore new file mode 100644 index 000000000000..91448661c374 --- /dev/null +++ b/packages/node/.gitignore @@ -0,0 +1 @@ +!diagnostics_channel.d.ts diff --git a/packages/node/src/integrations/diagnostics_channel.d.ts b/packages/node/src/integrations/diagnostics_channel.d.ts index 65cde75bb734..7689937f413c 100644 --- a/packages/node/src/integrations/diagnostics_channel.d.ts +++ b/packages/node/src/integrations/diagnostics_channel.d.ts @@ -166,6 +166,21 @@ declare module 'diagnostics_channel' { */ public unsubscribe(onMessage: ChannelListener): void; } + // https://github.com/nodejs/undici/blob/e6fc80f809d1217814c044f52ed40ef13f21e43c/types/diagnostics-channel.d.ts + interface Request { + origin?: string | URL; + completed: boolean; + // Originally was Dispatcher.HttpMethod, but did not want to vendor that in. + method?: string; + path: string; + headers: string; + addHeader(key: string, value: string): Request; + } + interface Response { + statusCode: number; + statusText: string; + headers: Array; + } } declare module 'node:diagnostics_channel' { export * from 'diagnostics_channel'; diff --git a/packages/node/src/integrations/undici.ts b/packages/node/src/integrations/undici.ts index 56a72ae7f4be..b05f04cc1eb9 100644 --- a/packages/node/src/integrations/undici.ts +++ b/packages/node/src/integrations/undici.ts @@ -1,7 +1,49 @@ -import type { Hub } from '@sentry/core'; +import type { Hub, Span } from '@sentry/core'; +import { stripUrlQueryAndFragment } from '@sentry/core'; import type { EventProcessor, Integration } from '@sentry/types'; +import { dynamicSamplingContextToSentryBaggageHeader, stringMatchesSomePattern } from '@sentry/utils'; import type DiagnosticsChannel from 'diagnostics_channel'; +import type { NodeClient } from '../client'; +import { isSentryRequest } from './utils/http'; + +enum ChannelName { + // https://github.com/nodejs/undici/blob/e6fc80f809d1217814c044f52ed40ef13f21e43c/docs/api/DiagnosticsChannel.md#undicirequestcreate + RequestCreate = 'undici:request:create', + RequestEnd = 'undici:request:headers', + RequestError = 'undici:request:error', +} + +interface RequestWithSentry extends DiagnosticsChannel.Request { + __sentry__?: Span; +} + +interface RequestCreateMessage { + request: RequestWithSentry; +} + +interface RequestEndMessage { + request: RequestWithSentry; + response: DiagnosticsChannel.Response; +} + +interface RequestErrorMessage { + request: RequestWithSentry; + error: Error; +} + +interface UndiciOptions { + /** + * Whether breadcrumbs should be recorded for requests + * Defaults to true + */ + breadcrumbs: boolean; +} + +const DEFAULT_UNDICI_OPTIONS: UndiciOptions = { + breadcrumbs: true, +}; + /** */ export class Undici implements Integration { /** @@ -17,12 +59,21 @@ export class Undici implements Integration { // Have to hold all built channels in memory otherwise they get garbage collected // See: https://github.com/nodejs/node/pull/42714 // This has been fixed in Node 19+ - private _channels: Map = new Map(); + private _channels = new Set(); + + private readonly _options: UndiciOptions; + + public constructor(_options: UndiciOptions) { + this._options = { + ...DEFAULT_UNDICI_OPTIONS, + ..._options, + }; + } /** * @inheritDoc */ - public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void, _getCurrentHub: () => Hub): void { + public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { let ds: typeof DiagnosticsChannel | undefined; try { // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -35,12 +86,146 @@ export class Undici implements Integration { return; } - // https://github.com/nodejs/undici/blob/main/docs/api/DiagnosticsChannel.md - const undiciChannel = ds.channel('undici:request'); + // https://github.com/nodejs/undici/blob/e6fc80f809d1217814c044f52ed40ef13f21e43c/docs/api/DiagnosticsChannel.md + const requestCreateChannel = this._setupChannel(ds, ChannelName.RequestCreate); + requestCreateChannel.subscribe(message => { + const { request } = message as RequestCreateMessage; + + const url = new URL(request.path, request.origin); + const stringUrl = url.toString(); + + if (isSentryRequest(stringUrl)) { + return; + } + + const hub = getCurrentHub(); + const client = hub.getClient(); + const scope = hub.getScope(); + + const activeSpan = scope.getSpan(); + + if (activeSpan && client) { + const options = client.getOptions(); + + // eslint-disable-next-line deprecation/deprecation + const shouldCreateSpan = options.shouldCreateSpanForRequest + ? // eslint-disable-next-line deprecation/deprecation + options.shouldCreateSpanForRequest(stringUrl) + : true; + + if (shouldCreateSpan) { + const span = activeSpan.startChild({ + op: 'http.client', + description: `${request.method || 'GET'} ${stripUrlQueryAndFragment(stringUrl)}`, + data: { + 'http.query': `?${url.searchParams.toString()}`, + 'http.fragment': url.hash, + }, + }); + request.__sentry__ = span; + + // eslint-disable-next-line deprecation/deprecation + const shouldPropagate = options.tracePropagationTargets + ? // eslint-disable-next-line deprecation/deprecation + stringMatchesSomePattern(stringUrl, options.tracePropagationTargets) + : true; + + if (shouldPropagate) { + // TODO: Only do this based on tracePropagationTargets + request.addHeader('sentry-trace', span.toTraceparent()); + if (span.transaction) { + const dynamicSamplingContext = span.transaction.getDynamicSamplingContext(); + const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); + if (sentryBaggageHeader) { + request.addHeader('baggage', sentryBaggageHeader); + } + } + } + } + } + }); + + const requestEndChannel = this._setupChannel(ds, ChannelName.RequestEnd); + requestEndChannel.subscribe(message => { + const { request, response } = message as RequestEndMessage; + + const url = new URL(request.path, request.origin); + const stringUrl = url.toString(); + + if (isSentryRequest(stringUrl)) { + return; + } + + const span = request.__sentry__; + if (span) { + span.setHttpStatus(response.statusCode); + span.finish(); + } + + if (this._options.breadcrumbs) { + getCurrentHub().addBreadcrumb( + { + category: 'http', + data: { + method: request.method, + status_code: response.statusCode, + url: stringUrl, + }, + type: 'http', + }, + { + event: 'response', + request, + response, + }, + ); + } + }); + + const requestErrorChannel = this._setupChannel(ds, ChannelName.RequestError); + requestErrorChannel.subscribe(message => { + const { request } = message as RequestErrorMessage; + + const url = new URL(request.path, request.origin); + const stringUrl = url.toString(); + + if (isSentryRequest(stringUrl)) { + return; + } + + const span = request.__sentry__; + if (span) { + span.setStatus('internal_error'); + span.finish(); + } + + if (this._options.breadcrumbs) { + getCurrentHub().addBreadcrumb( + { + category: 'http', + data: { + method: request.method, + url: stringUrl, + }, + level: 'error', + type: 'http', + }, + { + event: 'error', + request, + }, + ); + } + }); } - private _setupChannel(name: Parameters[0]): void { - const channel = DiagnosticsChannel.channel(name); - if (node) + /** */ + private _setupChannel( + ds: typeof DiagnosticsChannel, + name: Parameters[0], + ): DiagnosticsChannel.Channel { + const channel = ds.channel(name); + this._channels.add(channel); + return channel; } } From 17ea1af436722c04da9c7426cf0669c51c0f96c6 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 23 Mar 2023 15:10:28 +0100 Subject: [PATCH 03/11] this is gonna be a pain to test --- packages/node/src/integrations/index.ts | 1 + packages/node/src/integrations/undici.ts | 16 +++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/node/src/integrations/index.ts b/packages/node/src/integrations/index.ts index 167a482e5b5f..79fd4f93541a 100644 --- a/packages/node/src/integrations/index.ts +++ b/packages/node/src/integrations/index.ts @@ -8,3 +8,4 @@ export { ContextLines } from './contextlines'; export { Context } from './context'; export { RequestData } from './requestdata'; export { LocalVariables } from './localvariables'; +export { Undici } from './undici'; diff --git a/packages/node/src/integrations/undici.ts b/packages/node/src/integrations/undici.ts index b05f04cc1eb9..b5e8491bd9df 100644 --- a/packages/node/src/integrations/undici.ts +++ b/packages/node/src/integrations/undici.ts @@ -63,7 +63,7 @@ export class Undici implements Integration { private readonly _options: UndiciOptions; - public constructor(_options: UndiciOptions) { + public constructor(_options: Partial = {}) { this._options = { ...DEFAULT_UNDICI_OPTIONS, ..._options, @@ -114,13 +114,19 @@ export class Undici implements Integration { : true; if (shouldCreateSpan) { + const data: Record = {}; + const params = url.searchParams.toString(); + if (params) { + data['http.query'] = `?${params}`; + } + if (url.hash) { + data['http.fragment'] = url.hash; + } + const span = activeSpan.startChild({ op: 'http.client', description: `${request.method || 'GET'} ${stripUrlQueryAndFragment(stringUrl)}`, - data: { - 'http.query': `?${url.searchParams.toString()}`, - 'http.fragment': url.hash, - }, + data, }); request.__sentry__ = span; From c9cb2e9004628d5e2fa6a506616c32461c1a8f36 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 27 Mar 2023 13:47:36 +0200 Subject: [PATCH 04/11] update to latest dc --- .../src/integrations/diagnostics_channel.d.ts | 41 ++++++++++++++++++- packages/node/src/integrations/undici.ts | 36 ++++------------ 2 files changed, 49 insertions(+), 28 deletions(-) diff --git a/packages/node/src/integrations/diagnostics_channel.d.ts b/packages/node/src/integrations/diagnostics_channel.d.ts index 7689937f413c..d58f03d057b6 100644 --- a/packages/node/src/integrations/diagnostics_channel.d.ts +++ b/packages/node/src/integrations/diagnostics_channel.d.ts @@ -1,4 +1,4 @@ -// Vendored from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/8fc6d58e6434810867a9483e2107ea51bfca9153/types/node/diagnostics_channel.d.ts +// Vendored from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/5a94716c6788f654aea7999a5fc28f4f1e7c48ad/types/node/diagnostics_channel.d.ts // License: // This project is licensed under the MIT license. @@ -77,6 +77,45 @@ declare module 'diagnostics_channel' { */ function channel(name: string | symbol): Channel; type ChannelListener = (message: unknown, name: string | symbol) => void; + /** + * Register a message handler to subscribe to this channel. This message handler will be run synchronously + * whenever a message is published to the channel. Any errors thrown in the message handler will + * trigger an 'uncaughtException'. + * + * ```js + * import diagnostics_channel from 'diagnostics_channel'; + * + * diagnostics_channel.subscribe('my-channel', (message, name) => { + * // Received data + * }); + * ``` + * + * @since v18.7.0, v16.17.0 + * @param name The channel name + * @param onMessage The handler to receive channel messages + */ + function subscribe(name: string | symbol, onMessage: ChannelListener): void; + /** + * Remove a message handler previously registered to this channel with diagnostics_channel.subscribe(name, onMessage). + * + * ```js + * import diagnostics_channel from 'diagnostics_channel'; + * + * function onMessage(message, name) { + * // Received data + * } + * + * diagnostics_channel.subscribe('my-channel', onMessage); + * + * diagnostics_channel.unsubscribe('my-channel', onMessage); + * ``` + * + * @since v18.7.0, v16.17.0 + * @param name The channel name + * @param onMessage The previous subscribed handler to remove + * @returns `true` if the handler was found, `false` otherwise + */ + function unsubscribe(name: string | symbol, onMessage: ChannelListener): boolean; /** * The class `Channel` represents an individual named channel within the data * pipeline. It is use to track subscribers and to publish messages when there diff --git a/packages/node/src/integrations/undici.ts b/packages/node/src/integrations/undici.ts index b5e8491bd9df..9e6e45ddd779 100644 --- a/packages/node/src/integrations/undici.ts +++ b/packages/node/src/integrations/undici.ts @@ -56,11 +56,6 @@ export class Undici implements Integration { */ public name: string = Undici.id; - // Have to hold all built channels in memory otherwise they get garbage collected - // See: https://github.com/nodejs/node/pull/42714 - // This has been fixed in Node 19+ - private _channels = new Set(); - private readonly _options: UndiciOptions; public constructor(_options: Partial = {}) { @@ -82,13 +77,12 @@ export class Undici implements Integration { // no-op } - if (!ds) { + if (!ds || !ds.subscribe) { return; } // https://github.com/nodejs/undici/blob/e6fc80f809d1217814c044f52ed40ef13f21e43c/docs/api/DiagnosticsChannel.md - const requestCreateChannel = this._setupChannel(ds, ChannelName.RequestCreate); - requestCreateChannel.subscribe(message => { + ds.subscribe(ChannelName.RequestCreate, message => { const { request } = message as RequestCreateMessage; const url = new URL(request.path, request.origin); @@ -105,12 +99,12 @@ export class Undici implements Integration { const activeSpan = scope.getSpan(); if (activeSpan && client) { - const options = client.getOptions(); + const clientOptions = client.getOptions(); // eslint-disable-next-line deprecation/deprecation - const shouldCreateSpan = options.shouldCreateSpanForRequest + const shouldCreateSpan = clientOptions.shouldCreateSpanForRequest ? // eslint-disable-next-line deprecation/deprecation - options.shouldCreateSpanForRequest(stringUrl) + clientOptions.shouldCreateSpanForRequest(stringUrl) : true; if (shouldCreateSpan) { @@ -131,9 +125,9 @@ export class Undici implements Integration { request.__sentry__ = span; // eslint-disable-next-line deprecation/deprecation - const shouldPropagate = options.tracePropagationTargets + const shouldPropagate = clientOptions.tracePropagationTargets ? // eslint-disable-next-line deprecation/deprecation - stringMatchesSomePattern(stringUrl, options.tracePropagationTargets) + stringMatchesSomePattern(stringUrl, clientOptions.tracePropagationTargets) : true; if (shouldPropagate) { @@ -151,8 +145,7 @@ export class Undici implements Integration { } }); - const requestEndChannel = this._setupChannel(ds, ChannelName.RequestEnd); - requestEndChannel.subscribe(message => { + ds.subscribe(ChannelName.RequestEnd, message => { const { request, response } = message as RequestEndMessage; const url = new URL(request.path, request.origin); @@ -188,8 +181,7 @@ export class Undici implements Integration { } }); - const requestErrorChannel = this._setupChannel(ds, ChannelName.RequestError); - requestErrorChannel.subscribe(message => { + ds.subscribe(ChannelName.RequestError, message => { const { request } = message as RequestErrorMessage; const url = new URL(request.path, request.origin); @@ -224,14 +216,4 @@ export class Undici implements Integration { } }); } - - /** */ - private _setupChannel( - ds: typeof DiagnosticsChannel, - name: Parameters[0], - ): DiagnosticsChannel.Channel { - const channel = ds.channel(name); - this._channels.add(channel); - return channel; - } } From c750db5582e7a4355abdaf4794e61c8e49fcb66f Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 27 Mar 2023 16:25:10 +0200 Subject: [PATCH 05/11] re-organize --- .../{undici.ts => undici/index.ts} | 41 +-- .../types.ts} | 244 ++++++++++-------- 2 files changed, 148 insertions(+), 137 deletions(-) rename packages/node/src/integrations/{undici.ts => undici/index.ts} (85%) rename packages/node/src/integrations/{diagnostics_channel.d.ts => undici/types.ts} (53%) diff --git a/packages/node/src/integrations/undici.ts b/packages/node/src/integrations/undici/index.ts similarity index 85% rename from packages/node/src/integrations/undici.ts rename to packages/node/src/integrations/undici/index.ts index 9e6e45ddd779..77e66813f729 100644 --- a/packages/node/src/integrations/undici.ts +++ b/packages/node/src/integrations/undici/index.ts @@ -1,38 +1,23 @@ -import type { Hub, Span } from '@sentry/core'; -import { stripUrlQueryAndFragment } from '@sentry/core'; +import type { Hub } from '@sentry/core'; import type { EventProcessor, Integration } from '@sentry/types'; -import { dynamicSamplingContextToSentryBaggageHeader, stringMatchesSomePattern } from '@sentry/utils'; -import type DiagnosticsChannel from 'diagnostics_channel'; +import { + dynamicSamplingContextToSentryBaggageHeader, + stringMatchesSomePattern, + stripUrlQueryAndFragment, +} from '@sentry/utils'; -import type { NodeClient } from '../client'; -import { isSentryRequest } from './utils/http'; +import type { NodeClient } from '../../client'; +import { isSentryRequest } from '../utils/http'; +import type { DiagnosticsChannel, RequestCreateMessage, RequestEndMessage, RequestErrorMessage } from './types'; -enum ChannelName { +export enum ChannelName { // https://github.com/nodejs/undici/blob/e6fc80f809d1217814c044f52ed40ef13f21e43c/docs/api/DiagnosticsChannel.md#undicirequestcreate RequestCreate = 'undici:request:create', RequestEnd = 'undici:request:headers', RequestError = 'undici:request:error', } -interface RequestWithSentry extends DiagnosticsChannel.Request { - __sentry__?: Span; -} - -interface RequestCreateMessage { - request: RequestWithSentry; -} - -interface RequestEndMessage { - request: RequestWithSentry; - response: DiagnosticsChannel.Response; -} - -interface RequestErrorMessage { - request: RequestWithSentry; - error: Error; -} - -interface UndiciOptions { +export interface UndiciOptions { /** * Whether breadcrumbs should be recorded for requests * Defaults to true @@ -69,10 +54,10 @@ export class Undici implements Integration { * @inheritDoc */ public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { - let ds: typeof DiagnosticsChannel | undefined; + let ds: DiagnosticsChannel | undefined; try { // eslint-disable-next-line @typescript-eslint/no-var-requires - ds = require('diagnostics_channel') as typeof DiagnosticsChannel; + ds = require('diagnostics_channel') as DiagnosticsChannel; } catch (e) { // no-op } diff --git a/packages/node/src/integrations/diagnostics_channel.d.ts b/packages/node/src/integrations/undici/types.ts similarity index 53% rename from packages/node/src/integrations/diagnostics_channel.d.ts rename to packages/node/src/integrations/undici/types.ts index d58f03d057b6..9cbafeacfbb4 100644 --- a/packages/node/src/integrations/diagnostics_channel.d.ts +++ b/packages/node/src/integrations/undici/types.ts @@ -1,5 +1,7 @@ // Vendored from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/5a94716c6788f654aea7999a5fc28f4f1e7c48ad/types/node/diagnostics_channel.d.ts +import type { Span } from '@sentry/core'; + // License: // This project is licensed under the MIT license. // Copyrights are respective of each contributor listed at the beginning of each definition file. @@ -17,6 +19,9 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // Vendored code starts here: + +type ChannelListener = (message: unknown, name: string | symbol) => void; + /** * The `diagnostics_channel` module provides an API to create named channels * to report arbitrary message data for diagnostics purposes. @@ -41,7 +46,7 @@ * @experimental * @see [source](https://github.com/nodejs/node/blob/v18.0.0/lib/diagnostics_channel.js) */ -declare module 'diagnostics_channel' { +export interface DiagnosticsChannel { /** * Check if there are active subscribers to the named channel. This is helpful if * the message you want to send might be expensive to prepare. @@ -60,7 +65,7 @@ declare module 'diagnostics_channel' { * @param name The channel name * @return If there are active subscribers */ - function hasSubscribers(name: string | symbol): boolean; + hasSubscribers(name: string | symbol): boolean; /** * This is the primary entry-point for anyone wanting to interact with a named * channel. It produces a channel object which is optimized to reduce overhead at @@ -75,8 +80,7 @@ declare module 'diagnostics_channel' { * @param name The channel name * @return The named channel object */ - function channel(name: string | symbol): Channel; - type ChannelListener = (message: unknown, name: string | symbol) => void; + channel(name: string | symbol): Channel; /** * Register a message handler to subscribe to this channel. This message handler will be run synchronously * whenever a message is published to the channel. Any errors thrown in the message handler will @@ -94,7 +98,7 @@ declare module 'diagnostics_channel' { * @param name The channel name * @param onMessage The handler to receive channel messages */ - function subscribe(name: string | symbol, onMessage: ChannelListener): void; + subscribe(name: string | symbol, onMessage: ChannelListener): void; /** * Remove a message handler previously registered to this channel with diagnostics_channel.subscribe(name, onMessage). * @@ -115,112 +119,134 @@ declare module 'diagnostics_channel' { * @param onMessage The previous subscribed handler to remove * @returns `true` if the handler was found, `false` otherwise */ - function unsubscribe(name: string | symbol, onMessage: ChannelListener): boolean; + unsubscribe(name: string | symbol, onMessage: ChannelListener): boolean; +} + +/** + * The class `Channel` represents an individual named channel within the data + * pipeline. It is use to track subscribers and to publish messages when there + * are subscribers present. It exists as a separate object to avoid channel + * lookups at publish time, enabling very fast publish speeds and allowing + * for heavy use while incurring very minimal cost. Channels are created with {@link channel}, constructing a channel directly + * with `new Channel(name)` is not supported. + * @since v15.1.0, v14.17.0 + */ +interface ChannelI { + readonly name: string | symbol; + /** + * Check if there are active subscribers to this channel. This is helpful if + * the message you want to send might be expensive to prepare. + * + * This API is optional but helpful when trying to publish messages from very + * performance-sensitive code. + * + * ```js + * import diagnostics_channel from 'diagnostics_channel'; + * + * const channel = diagnostics_channel.channel('my-channel'); + * + * if (channel.hasSubscribers) { + * // There are subscribers, prepare and publish message + * } + * ``` + * @since v15.1.0, v14.17.0 + */ + readonly hasSubscribers: boolean; + + /** + * Publish a message to any subscribers to the channel. This will + * trigger message handlers synchronously so they will execute within + * the same context. + * + * ```js + * import diagnostics_channel from 'diagnostics_channel'; + * + * const channel = diagnostics_channel.channel('my-channel'); + * + * channel.publish({ + * some: 'message' + * }); + * ``` + * @since v15.1.0, v14.17.0 + * @param message The message to send to the channel subscribers + */ + publish(message: unknown): void; + /** + * Register a message handler to subscribe to this channel. This message handler + * will be run synchronously whenever a message is published to the channel. Any + * errors thrown in the message handler will trigger an `'uncaughtException'`. + * + * ```js + * import diagnostics_channel from 'diagnostics_channel'; + * + * const channel = diagnostics_channel.channel('my-channel'); + * + * channel.subscribe((message, name) => { + * // Received data + * }); + * ``` + * @since v15.1.0, v14.17.0 + * @param onMessage The handler to receive channel messages + */ + subscribe(onMessage: ChannelListener): void; /** - * The class `Channel` represents an individual named channel within the data - * pipeline. It is use to track subscribers and to publish messages when there - * are subscribers present. It exists as a separate object to avoid channel - * lookups at publish time, enabling very fast publish speeds and allowing - * for heavy use while incurring very minimal cost. Channels are created with {@link channel}, constructing a channel directly - * with `new Channel(name)` is not supported. + * Remove a message handler previously registered to this channel with `channel.subscribe(onMessage)`. + * + * ```js + * import diagnostics_channel from 'diagnostics_channel'; + * + * const channel = diagnostics_channel.channel('my-channel'); + * + * function onMessage(message, name) { + * // Received data + * } + * + * channel.subscribe(onMessage); + * + * channel.unsubscribe(onMessage); + * ``` * @since v15.1.0, v14.17.0 + * @param onMessage The previous subscribed handler to remove + * @return `true` if the handler was found, `false` otherwise. */ - class Channel { - public readonly name: string | symbol; - /** - * Check if there are active subscribers to this channel. This is helpful if - * the message you want to send might be expensive to prepare. - * - * This API is optional but helpful when trying to publish messages from very - * performance-sensitive code. - * - * ```js - * import diagnostics_channel from 'diagnostics_channel'; - * - * const channel = diagnostics_channel.channel('my-channel'); - * - * if (channel.hasSubscribers) { - * // There are subscribers, prepare and publish message - * } - * ``` - * @since v15.1.0, v14.17.0 - */ - public readonly hasSubscribers: boolean; - private constructor(name: string | symbol); - /** - * Publish a message to any subscribers to the channel. This will - * trigger message handlers synchronously so they will execute within - * the same context. - * - * ```js - * import diagnostics_channel from 'diagnostics_channel'; - * - * const channel = diagnostics_channel.channel('my-channel'); - * - * channel.publish({ - * some: 'message' - * }); - * ``` - * @since v15.1.0, v14.17.0 - * @param message The message to send to the channel subscribers - */ - public publish(message: unknown): void; - /** - * Register a message handler to subscribe to this channel. This message handler - * will be run synchronously whenever a message is published to the channel. Any - * errors thrown in the message handler will trigger an `'uncaughtException'`. - * - * ```js - * import diagnostics_channel from 'diagnostics_channel'; - * - * const channel = diagnostics_channel.channel('my-channel'); - * - * channel.subscribe((message, name) => { - * // Received data - * }); - * ``` - * @since v15.1.0, v14.17.0 - * @param onMessage The handler to receive channel messages - */ - public subscribe(onMessage: ChannelListener): void; - /** - * Remove a message handler previously registered to this channel with `channel.subscribe(onMessage)`. - * - * ```js - * import diagnostics_channel from 'diagnostics_channel'; - * - * const channel = diagnostics_channel.channel('my-channel'); - * - * function onMessage(message, name) { - * // Received data - * } - * - * channel.subscribe(onMessage); - * - * channel.unsubscribe(onMessage); - * ``` - * @since v15.1.0, v14.17.0 - * @param onMessage The previous subscribed handler to remove - * @return `true` if the handler was found, `false` otherwise. - */ - public unsubscribe(onMessage: ChannelListener): void; - } - // https://github.com/nodejs/undici/blob/e6fc80f809d1217814c044f52ed40ef13f21e43c/types/diagnostics-channel.d.ts - interface Request { - origin?: string | URL; - completed: boolean; - // Originally was Dispatcher.HttpMethod, but did not want to vendor that in. - method?: string; - path: string; - headers: string; - addHeader(key: string, value: string): Request; - } - interface Response { - statusCode: number; - statusText: string; - headers: Array; - } + unsubscribe(onMessage: ChannelListener): void; +} + +export interface Channel extends ChannelI { + new (name: string | symbol): void; +} + +// https://github.com/nodejs/undici/blob/e6fc80f809d1217814c044f52ed40ef13f21e43c/types/diagnostics-channel.d.ts +export interface UndiciRequest { + origin?: string | URL; + completed: boolean; + // Originally was Dispatcher.HttpMethod, but did not want to vendor that in. + method?: string; + path: string; + headers: string; + addHeader(key: string, value: string): Request; } -declare module 'node:diagnostics_channel' { - export * from 'diagnostics_channel'; + +export interface UndiciResponse { + statusCode: number; + statusText: string; + headers: Array; +} + +export interface RequestWithSentry extends UndiciRequest { + __sentry__?: Span; +} + +export interface RequestCreateMessage { + request: RequestWithSentry; +} + +export interface RequestEndMessage { + request: RequestWithSentry; + response: UndiciResponse; +} + +export interface RequestErrorMessage { + request: RequestWithSentry; + error: Error; } From 6ddbda0e97fd3999d644716d4b8f701499b23dee Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 27 Mar 2023 16:26:11 +0200 Subject: [PATCH 06/11] remove .gitignore --- packages/node/.gitignore | 1 - 1 file changed, 1 deletion(-) delete mode 100644 packages/node/.gitignore diff --git a/packages/node/.gitignore b/packages/node/.gitignore deleted file mode 100644 index 91448661c374..000000000000 --- a/packages/node/.gitignore +++ /dev/null @@ -1 +0,0 @@ -!diagnostics_channel.d.ts From 5c4509808365d8dcaefb579f93eb5c286a44e93f Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 27 Mar 2023 16:59:46 +0200 Subject: [PATCH 07/11] add docs string --- packages/node/src/integrations/undici/index.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/node/src/integrations/undici/index.ts b/packages/node/src/integrations/undici/index.ts index 77e66813f729..9a571881553f 100644 --- a/packages/node/src/integrations/undici/index.ts +++ b/packages/node/src/integrations/undici/index.ts @@ -29,7 +29,14 @@ const DEFAULT_UNDICI_OPTIONS: UndiciOptions = { breadcrumbs: true, }; -/** */ +/** + * Instruments outgoing HTTP requests made with the `undici` package via + * Node's `diagnostics_channel` API. + * + * Supports Undici 4.7.0 or higher. + * + * Requires Node 18.9.0 or higher. + */ export class Undici implements Integration { /** * @inheritDoc From 47c921eddf3908ab965cba5d2d81353946cadf44 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 27 Mar 2023 17:37:31 +0200 Subject: [PATCH 08/11] make node 14 only --- packages/node/src/integrations/undici/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/node/src/integrations/undici/index.ts b/packages/node/src/integrations/undici/index.ts index 9a571881553f..c4a6ba455143 100644 --- a/packages/node/src/integrations/undici/index.ts +++ b/packages/node/src/integrations/undici/index.ts @@ -2,6 +2,7 @@ import type { Hub } from '@sentry/core'; import type { EventProcessor, Integration } from '@sentry/types'; import { dynamicSamplingContextToSentryBaggageHeader, + parseSemver, stringMatchesSomePattern, stripUrlQueryAndFragment, } from '@sentry/utils'; @@ -10,6 +11,8 @@ import type { NodeClient } from '../../client'; import { isSentryRequest } from '../utils/http'; import type { DiagnosticsChannel, RequestCreateMessage, RequestEndMessage, RequestErrorMessage } from './types'; +const NODE_VERSION = parseSemver(process.versions.node); + export enum ChannelName { // https://github.com/nodejs/undici/blob/e6fc80f809d1217814c044f52ed40ef13f21e43c/docs/api/DiagnosticsChannel.md#undicirequestcreate RequestCreate = 'undici:request:create', @@ -61,6 +64,11 @@ export class Undici implements Integration { * @inheritDoc */ public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { + // Requires Node 14+ to use the diagnostics_channel API. + if (NODE_VERSION.major && NODE_VERSION.major < 14) { + return; + } + let ds: DiagnosticsChannel | undefined; try { // eslint-disable-next-line @typescript-eslint/no-var-requires From bda623e48916019a2339df4c6c31716dd24282a6 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 27 Mar 2023 19:26:19 +0200 Subject: [PATCH 09/11] use dynamic require --- packages/node/src/integrations/undici/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/node/src/integrations/undici/index.ts b/packages/node/src/integrations/undici/index.ts index c4a6ba455143..38b2167c2143 100644 --- a/packages/node/src/integrations/undici/index.ts +++ b/packages/node/src/integrations/undici/index.ts @@ -5,6 +5,7 @@ import { parseSemver, stringMatchesSomePattern, stripUrlQueryAndFragment, + dynamicRequire, } from '@sentry/utils'; import type { NodeClient } from '../../client'; @@ -64,15 +65,15 @@ export class Undici implements Integration { * @inheritDoc */ public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { - // Requires Node 14+ to use the diagnostics_channel API. - if (NODE_VERSION.major && NODE_VERSION.major < 14) { + // Requires Node 16+ to use the diagnostics_channel API. + if (NODE_VERSION.major && NODE_VERSION.major < 16) { return; } let ds: DiagnosticsChannel | undefined; try { // eslint-disable-next-line @typescript-eslint/no-var-requires - ds = require('diagnostics_channel') as DiagnosticsChannel; + ds = dynamicRequire(module, 'diagnostics_channel') as DiagnosticsChannel; } catch (e) { // no-op } From a1b087db81aa5a58b7f2e4dc11e0a5aee6f9c9e7 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 27 Mar 2023 19:27:03 +0200 Subject: [PATCH 10/11] requires Node 16 --- packages/node/src/integrations/undici/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node/src/integrations/undici/index.ts b/packages/node/src/integrations/undici/index.ts index 38b2167c2143..1fa6e70bc326 100644 --- a/packages/node/src/integrations/undici/index.ts +++ b/packages/node/src/integrations/undici/index.ts @@ -39,7 +39,7 @@ const DEFAULT_UNDICI_OPTIONS: UndiciOptions = { * * Supports Undici 4.7.0 or higher. * - * Requires Node 18.9.0 or higher. + * Requires Node 16.17.0 or higher. */ export class Undici implements Integration { /** From e5f69fc94691322e72671efe5123d58a382547f4 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 27 Mar 2023 19:30:01 +0200 Subject: [PATCH 11/11] lint --- packages/node/src/integrations/undici/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node/src/integrations/undici/index.ts b/packages/node/src/integrations/undici/index.ts index 1fa6e70bc326..8edc5e326cea 100644 --- a/packages/node/src/integrations/undici/index.ts +++ b/packages/node/src/integrations/undici/index.ts @@ -1,11 +1,11 @@ import type { Hub } from '@sentry/core'; import type { EventProcessor, Integration } from '@sentry/types'; import { + dynamicRequire, dynamicSamplingContextToSentryBaggageHeader, parseSemver, stringMatchesSomePattern, stripUrlQueryAndFragment, - dynamicRequire, } from '@sentry/utils'; import type { NodeClient } from '../../client';