diff --git a/packages/browser/src/index.bundle.base.ts b/packages/browser/src/index.bundle.base.ts index 5ccb0f1b1cf2..8570a426a7e7 100644 --- a/packages/browser/src/index.bundle.base.ts +++ b/packages/browser/src/index.bundle.base.ts @@ -1,21 +1 @@ -import type { IntegrationFn } from '@sentry/types/src'; - export * from './exports'; - -import type { Integration } from '@sentry/types'; - -import { WINDOW } from './helpers'; - -let windowIntegrations = {}; - -// This block is needed to add compatibility with the integrations packages when used with a CDN -if (WINDOW.Sentry && WINDOW.Sentry.Integrations) { - windowIntegrations = WINDOW.Sentry.Integrations; -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const INTEGRATIONS: Record Integration) | IntegrationFn> = { - ...windowIntegrations, -}; - -export { INTEGRATIONS as Integrations }; diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 8608944e3155..3c52590289cb 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -1,22 +1,5 @@ export * from './exports'; -import { WINDOW } from './helpers'; - -let windowIntegrations = {}; - -// This block is needed to add compatibility with the integrations packages when used with a CDN -if (WINDOW.Sentry && WINDOW.Sentry.Integrations) { - windowIntegrations = WINDOW.Sentry.Integrations; -} - -/** @deprecated Import the integration function directly, e.g. `inboundFiltersIntegration()` instead of `new Integrations.InboundFilter(). */ -const INTEGRATIONS = { - ...windowIntegrations, -}; - -// eslint-disable-next-line deprecation/deprecation -export { INTEGRATIONS as Integrations }; - export { reportingObserverIntegration } from './integrations/reportingobserver'; export { httpClientIntegration } from './integrations/httpclient'; export { contextLinesIntegration } from './integrations/contextlines'; diff --git a/packages/browser/test/unit/index.bundle.feedback.test.ts b/packages/browser/test/unit/index.bundle.feedback.test.ts index 4ccb6a9dc458..5a3451cb3ef0 100644 --- a/packages/browser/test/unit/index.bundle.feedback.test.ts +++ b/packages/browser/test/unit/index.bundle.feedback.test.ts @@ -5,10 +5,6 @@ import * as TracingReplayBundle from '../../src/index.bundle.feedback'; describe('index.bundle.feedback', () => { it('has correct exports', () => { - Object.keys(TracingReplayBundle.Integrations).forEach(key => { - expect((TracingReplayBundle.Integrations[key] as any).id).toStrictEqual(expect.any(String)); - }); - expect(TracingReplayBundle.replayIntegration).toBe(replayIntegrationShim); expect(TracingReplayBundle.feedbackIntegration).toBe(feedbackIntegration); }); diff --git a/packages/browser/test/unit/index.bundle.replay.test.ts b/packages/browser/test/unit/index.bundle.replay.test.ts index 56356e262eea..479e6b23393b 100644 --- a/packages/browser/test/unit/index.bundle.replay.test.ts +++ b/packages/browser/test/unit/index.bundle.replay.test.ts @@ -5,10 +5,6 @@ import * as TracingReplayBundle from '../../src/index.bundle.replay'; describe('index.bundle.replay', () => { it('has correct exports', () => { - Object.keys(TracingReplayBundle.Integrations).forEach(key => { - expect((TracingReplayBundle.Integrations[key] as any).id).toStrictEqual(expect.any(String)); - }); - expect(TracingReplayBundle.replayIntegration).toBe(replayIntegration); expect(TracingReplayBundle.feedbackIntegration).toBe(feedbackIntegrationShim); }); diff --git a/packages/browser/test/unit/index.bundle.test.ts b/packages/browser/test/unit/index.bundle.test.ts index 91cc4dea5229..9ef9f38d8db5 100644 --- a/packages/browser/test/unit/index.bundle.test.ts +++ b/packages/browser/test/unit/index.bundle.test.ts @@ -4,10 +4,6 @@ import * as TracingBundle from '../../src/index.bundle'; describe('index.bundle', () => { it('has correct exports', () => { - Object.keys(TracingBundle.Integrations).forEach(key => { - expect((TracingBundle.Integrations[key] as any).name).toStrictEqual(expect.any(String)); - }); - expect(TracingBundle.replayIntegration).toBe(replayIntegrationShim); expect(TracingBundle.feedbackIntegration).toBe(feedbackIntegrationShim); }); diff --git a/packages/browser/test/unit/index.bundle.tracing.replay.feedback.test.ts b/packages/browser/test/unit/index.bundle.tracing.replay.feedback.test.ts index 49c23d9685bb..279d72079bc5 100644 --- a/packages/browser/test/unit/index.bundle.tracing.replay.feedback.test.ts +++ b/packages/browser/test/unit/index.bundle.tracing.replay.feedback.test.ts @@ -5,10 +5,6 @@ import * as TracingReplayFeedbackBundle from '../../src/index.bundle.tracing.rep describe('index.bundle.tracing.replay.feedback', () => { it('has correct exports', () => { - Object.keys(TracingReplayFeedbackBundle.Integrations).forEach(key => { - expect((TracingReplayFeedbackBundle.Integrations[key] as any).id).toStrictEqual(expect.any(String)); - }); - expect(TracingReplayFeedbackBundle.replayIntegration).toBe(replayIntegration); expect(TracingReplayFeedbackBundle.browserTracingIntegration).toBe(browserTracingIntegration); expect(TracingReplayFeedbackBundle.feedbackIntegration).toBe(feedbackIntegration); diff --git a/packages/browser/test/unit/index.bundle.tracing.replay.test.ts b/packages/browser/test/unit/index.bundle.tracing.replay.test.ts index bdbb744f7873..6519f4525300 100644 --- a/packages/browser/test/unit/index.bundle.tracing.replay.test.ts +++ b/packages/browser/test/unit/index.bundle.tracing.replay.test.ts @@ -6,10 +6,6 @@ import * as TracingReplayBundle from '../../src/index.bundle.tracing.replay'; describe('index.bundle.tracing.replay', () => { it('has correct exports', () => { - Object.keys(TracingReplayBundle.Integrations).forEach(key => { - expect((TracingReplayBundle.Integrations[key] as any).id).toStrictEqual(expect.any(String)); - }); - expect(TracingReplayBundle.replayIntegration).toBe(replayIntegration); expect(TracingReplayBundle.browserTracingIntegration).toBe(browserTracingIntegration); diff --git a/packages/browser/test/unit/index.bundle.tracing.test.ts b/packages/browser/test/unit/index.bundle.tracing.test.ts index 4c8c37008fc4..62e40d0fad39 100644 --- a/packages/browser/test/unit/index.bundle.tracing.test.ts +++ b/packages/browser/test/unit/index.bundle.tracing.test.ts @@ -5,10 +5,6 @@ import * as TracingBundle from '../../src/index.bundle.tracing'; describe('index.bundle.tracing', () => { it('has correct exports', () => { - Object.keys(TracingBundle.Integrations).forEach(key => { - expect((TracingBundle.Integrations[key] as any).id).toStrictEqual(expect.any(String)); - }); - expect(TracingBundle.replayIntegration).toBe(replayIntegrationShim); expect(TracingBundle.browserTracingIntegration).toBe(browserTracingIntegration); expect(TracingBundle.feedbackIntegration).toBe(feedbackIntegrationShim); diff --git a/packages/nextjs/src/index.types.ts b/packages/nextjs/src/index.types.ts index 67d67c66c05c..70c4de63764d 100644 --- a/packages/nextjs/src/index.types.ts +++ b/packages/nextjs/src/index.types.ts @@ -23,8 +23,6 @@ export declare const getClient: typeof clientSdk.getClient; export declare const getRootSpan: typeof serverSdk.getRootSpan; export declare const continueTrace: typeof clientSdk.continueTrace; -export declare const Integrations: undefined; // TODO(v8): Remove this line. Can only be done when dependencies don't export `Integrations` anymore. - export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; diff --git a/packages/remix/src/index.types.ts b/packages/remix/src/index.types.ts index cbbf17998188..85bbb1a56235 100644 --- a/packages/remix/src/index.types.ts +++ b/packages/remix/src/index.types.ts @@ -12,10 +12,6 @@ import type { RemixOptions } from './utils/remixOptions'; /** Initializes Sentry Remix SDK */ export declare function init(options: RemixOptions): void; -// We export a merged Integrations object so that users can (at least typing-wise) use all integrations everywhere. -// eslint-disable-next-line deprecation/deprecation -export declare const Integrations: typeof clientSdk.Integrations; - export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; diff --git a/packages/tracing-internal/src/browser/index.ts b/packages/tracing-internal/src/browser/index.ts index 6d4168329dc4..948a09c0dda4 100644 --- a/packages/tracing-internal/src/browser/index.ts +++ b/packages/tracing-internal/src/browser/index.ts @@ -1,5 +1,3 @@ -export * from '../exports'; - export type { RequestInstrumentationOptions } from './request'; export { diff --git a/packages/tracing-internal/src/exports/index.ts b/packages/tracing-internal/src/exports/index.ts deleted file mode 100644 index b3a3e3a4b4ed..000000000000 --- a/packages/tracing-internal/src/exports/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { - hasTracingEnabled, - Transaction, -} from '@sentry/core'; -export { stripUrlQueryAndFragment, TRACEPARENT_REGEXP } from '@sentry/utils'; diff --git a/packages/tracing-internal/src/index.ts b/packages/tracing-internal/src/index.ts index 2dd4cf2f1768..019639e8c0b3 100644 --- a/packages/tracing-internal/src/index.ts +++ b/packages/tracing-internal/src/index.ts @@ -1,17 +1,3 @@ -export * from './exports'; - -export { - Apollo, - Express, - GraphQL, - Mongo, - Mysql, - Postgres, - Prisma, - lazyLoadedNodePerformanceMonitoringIntegrations, -} from './node'; -export type { LazyLoadedIntegration } from './node'; - export { browserTracingIntegration, startBrowserTracingNavigationSpan, diff --git a/packages/tracing-internal/src/node/index.ts b/packages/tracing-internal/src/node/index.ts deleted file mode 100644 index eac5910c32c7..000000000000 --- a/packages/tracing-internal/src/node/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from '../exports'; - -export * from './integrations'; diff --git a/packages/tracing-internal/src/node/integrations/apollo.ts b/packages/tracing-internal/src/node/integrations/apollo.ts deleted file mode 100644 index 17ba0ab15482..000000000000 --- a/packages/tracing-internal/src/node/integrations/apollo.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core'; -import { arrayify, fill, loadModule, logger } from '@sentry/utils'; - -import { DEBUG_BUILD } from '../../common/debug-build'; -import type { LazyLoadedIntegration } from './lazy'; - -interface ApolloOptions { - useNestjs?: boolean; -} - -type ApolloResolverGroup = { - [key: string]: () => unknown; -}; - -type ApolloModelResolvers = { - [key: string]: ApolloResolverGroup; -}; - -type GraphQLModule = { - GraphQLFactory: { - prototype: { - create: (resolvers: ApolloModelResolvers[]) => unknown; - }; - }; -}; - -type ApolloModule = { - ApolloServerBase: { - prototype: { - constructSchema: (config: unknown) => unknown; - }; - }; -}; - -/** Tracing integration for Apollo */ -export class Apollo implements LazyLoadedIntegration { - /** - * @inheritDoc - */ - public static id: string = 'Apollo'; - - /** - * @inheritDoc - */ - public name: string; - - private readonly _useNest: boolean; - - private _module?: GraphQLModule & ApolloModule; - - /** - * @inheritDoc - */ - public constructor( - options: ApolloOptions = { - useNestjs: false, - }, - ) { - this.name = Apollo.id; - this._useNest = !!options.useNestjs; - } - - /** @inheritdoc */ - public loadDependency(): (GraphQLModule & ApolloModule) | undefined { - if (this._useNest) { - this._module = this._module || loadModule('@nestjs/graphql'); - } else { - this._module = this._module || loadModule('apollo-server-core'); - } - - return this._module; - } - - /** - * @inheritDoc - */ - public setupOnce(): void { - if (this._useNest) { - const pkg = this.loadDependency(); - - if (!pkg) { - DEBUG_BUILD && logger.error('Apollo-NestJS Integration was unable to require @nestjs/graphql package.'); - return; - } - - /** - * Iterate over resolvers of NestJS ResolversExplorerService before schemas are constructed. - */ - fill( - pkg.GraphQLFactory.prototype, - 'mergeWithSchema', - function (orig: (this: unknown, ...args: unknown[]) => unknown) { - return function ( - this: { resolversExplorerService: { explore: () => ApolloModelResolvers[] } }, - ...args: unknown[] - ) { - fill(this.resolversExplorerService, 'explore', function (orig: () => ApolloModelResolvers[]) { - return function (this: unknown) { - const resolvers = arrayify(orig.call(this)); - - const instrumentedResolvers = instrumentResolvers(resolvers); - - return instrumentedResolvers; - }; - }); - - return orig.call(this, ...args); - }; - }, - ); - } else { - const pkg = this.loadDependency(); - - if (!pkg) { - DEBUG_BUILD && logger.error('Apollo Integration was unable to require apollo-server-core package.'); - return; - } - - /** - * Iterate over resolvers of the ApolloServer instance before schemas are constructed. - */ - fill(pkg.ApolloServerBase.prototype, 'constructSchema', function (orig: (config: unknown) => unknown) { - return function (this: { - config: { resolvers?: ApolloModelResolvers[]; schema?: unknown; modules?: unknown }; - }) { - if (!this.config.resolvers) { - if (DEBUG_BUILD) { - if (this.config.schema) { - logger.warn( - 'Apollo integration is not able to trace `ApolloServer` instances constructed via `schema` property.' + - 'If you are using NestJS with Apollo, please use `Sentry.Integrations.Apollo({ useNestjs: true })` instead.', - ); - logger.warn(); - } else if (this.config.modules) { - logger.warn( - 'Apollo integration is not able to trace `ApolloServer` instances constructed via `modules` property.', - ); - } - - logger.error('Skipping tracing as no resolvers found on the `ApolloServer` instance.'); - } - - return orig.call(this); - } - - const resolvers = arrayify(this.config.resolvers); - - this.config.resolvers = instrumentResolvers(resolvers); - - return orig.call(this); - }; - }); - } - } -} - -function instrumentResolvers(resolvers: ApolloModelResolvers[]): ApolloModelResolvers[] { - return resolvers.map(model => { - Object.keys(model).forEach(resolverGroupName => { - Object.keys(model[resolverGroupName]).forEach(resolverName => { - if (typeof model[resolverGroupName][resolverName] !== 'function') { - return; - } - - wrapResolver(model, resolverGroupName, resolverName); - }); - }); - - return model; - }); -} - -/** - * Wrap a single resolver which can be a parent of other resolvers and/or db operations. - */ -function wrapResolver(model: ApolloModelResolvers, resolverGroupName: string, resolverName: string): void { - fill(model[resolverGroupName], resolverName, function (orig: () => unknown | Promise) { - return function (this: unknown, ...args: unknown[]) { - return startSpan( - { - onlyIfParent: true, - name: `${resolverGroupName}.${resolverName}`, - op: 'graphql.resolve', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.graphql.apollo', - }, - }, - () => { - return orig.call(this, ...args); - }, - ); - }; - }); -} diff --git a/packages/tracing-internal/src/node/integrations/express.ts b/packages/tracing-internal/src/node/integrations/express.ts deleted file mode 100644 index 26c82a2ef8dd..000000000000 --- a/packages/tracing-internal/src/node/integrations/express.ts +++ /dev/null @@ -1,583 +0,0 @@ -/* eslint-disable max-lines */ -import type { Transaction } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; -import { startInactiveSpan, withActiveSpan } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON } from '@sentry/core'; -import type { Integration, PolymorphicRequest } from '@sentry/types'; -import { - GLOBAL_OBJ, - extractPathForTransaction, - getNumberOfUrlSegments, - isRegExp, - logger, - stripUrlQueryAndFragment, -} from '@sentry/utils'; - -import { DEBUG_BUILD } from '../../common/debug-build'; - -type Method = - | 'all' - | 'get' - | 'post' - | 'put' - | 'delete' - | 'patch' - | 'options' - | 'head' - | 'checkout' - | 'copy' - | 'lock' - | 'merge' - | 'mkactivity' - | 'mkcol' - | 'move' - | 'm-search' - | 'notify' - | 'purge' - | 'report' - | 'search' - | 'subscribe' - | 'trace' - | 'unlock' - | 'unsubscribe' - | 'use'; - -type Router = { - [method in Method]: (...args: any) => any; // eslint-disable-line @typescript-eslint/no-explicit-any -}; - -/* Extend the PolymorphicRequest type with a patched parameter to build a reconstructed route */ -type PatchedRequest = PolymorphicRequest & { _reconstructedRoute?: string; _hasParameters?: boolean }; - -/* Types used for patching the express router prototype */ -type ExpressRouter = Router & { - _router?: ExpressRouter; - stack?: Layer[]; - lazyrouter?: () => void; - settings?: unknown; - process_params: ( - layer: Layer, - called: unknown, - req: PatchedRequest, - res: ExpressResponse, - done: () => void, - ) => unknown; -}; - -type Layer = { - match: (path: string) => boolean; - handle_request: (req: PatchedRequest, res: ExpressResponse, next: () => void) => void; - route?: { path: RouteType | RouteType[] }; - path?: string; - regexp?: RegExp; - keys?: { name: string | number; offset: number; optional: boolean }[]; -}; - -type RouteType = string | RegExp; - -interface ExpressResponse { - once(name: string, callback: () => void): void; -} - -/** - * Internal helper for `__sentry_transaction` - * @hidden - */ -interface SentryTracingResponse { - __sentry_transaction?: Transaction; -} - -/** - * Express integration - * - * Provides an request and error handler for Express framework as well as tracing capabilities - */ -export class Express implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'Express'; - - /** - * @inheritDoc - */ - public name: string; - - /** - * Express App instance - */ - private readonly _router?: Router; - private readonly _methods?: Method[]; - - /** - * @inheritDoc - */ - public constructor(options: { app?: Router; router?: Router; methods?: Method[] } = {}) { - this.name = Express.id; - this._router = options.router || options.app; - this._methods = (Array.isArray(options.methods) ? options.methods : []).concat('use'); - } - - /** - * @inheritDoc - */ - public setupOnce(): void { - if (!this._router) { - DEBUG_BUILD && logger.error('ExpressIntegration is missing an Express instance'); - return; - } - - instrumentMiddlewares(this._router, this._methods); - instrumentRouter(this._router as ExpressRouter); - } -} - -/** - * Wraps original middleware function in a tracing call, which stores the info about the call as a span, - * and finishes it once the middleware is done invoking. - * - * Express middlewares have 3 various forms, thus we have to take care of all of them: - * // sync - * app.use(function (req, res) { ... }) - * // async - * app.use(function (req, res, next) { ... }) - * // error handler - * app.use(function (err, req, res, next) { ... }) - * - * They all internally delegate to the `router[method]` of the given application instance. - */ -// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any -function wrap(fn: Function, method: Method): (...args: any[]) => void { - const arity = fn.length; - - switch (arity) { - case 2: { - return function (this: NodeJS.Global, req: unknown, res: ExpressResponse & SentryTracingResponse): void { - const transaction = res.__sentry_transaction; - if (transaction) { - const span = withActiveSpan(transaction, () => { - return startInactiveSpan({ - name: fn.name, - op: `middleware.express.${method}`, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.express', - }, - }); - }); - res.once('finish', () => { - span.end(); - }); - } - return fn.call(this, req, res); - }; - } - case 3: { - return function ( - this: NodeJS.Global, - req: unknown, - res: ExpressResponse & SentryTracingResponse, - next: () => void, - ): void { - const transaction = res.__sentry_transaction; - const span = transaction - ? withActiveSpan(transaction, () => { - return startInactiveSpan({ - name: fn.name, - op: `middleware.express.${method}`, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.express', - }, - }); - }) - : undefined; - fn.call(this, req, res, function (this: NodeJS.Global, ...args: unknown[]): void { - span?.end(); - next.call(this, ...args); - }); - }; - } - case 4: { - return function ( - this: NodeJS.Global, - err: Error, - req: Request, - res: Response & SentryTracingResponse, - next: () => void, - ): void { - const transaction = res.__sentry_transaction; - const span = transaction - ? withActiveSpan(transaction, () => { - return startInactiveSpan({ - name: fn.name, - op: `middleware.express.${method}`, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.express', - }, - }); - }) - : undefined; - fn.call(this, err, req, res, function (this: NodeJS.Global, ...args: unknown[]): void { - span?.end(); - next.call(this, ...args); - }); - }; - } - default: { - throw new Error(`Express middleware takes 2-4 arguments. Got: ${arity}`); - } - } -} - -/** - * Takes all the function arguments passed to the original `app` or `router` method, eg. `app.use` or `router.use` - * and wraps every function, as well as array of functions with a call to our `wrap` method. - * We have to take care of the arrays as well as iterate over all of the arguments, - * as `app.use` can accept middlewares in few various forms. - * - * app.use([], ) - * app.use([], , ...) - * app.use([], ...[]) - */ -function wrapMiddlewareArgs(args: unknown[], method: Method): unknown[] { - return args.map((arg: unknown) => { - if (typeof arg === 'function') { - return wrap(arg, method); - } - - if (Array.isArray(arg)) { - return arg.map((a: unknown) => { - if (typeof a === 'function') { - return wrap(a, method); - } - return a; - }); - } - - return arg; - }); -} - -/** - * Patches original router to utilize our tracing functionality - */ -function patchMiddleware(router: Router, method: Method): Router { - const originalCallback = router[method]; - - router[method] = function (...args: unknown[]): void { - return originalCallback.call(this, ...wrapMiddlewareArgs(args, method)); - }; - - return router; -} - -/** - * Patches original router methods - */ -function instrumentMiddlewares(router: Router, methods: Method[] = []): void { - methods.forEach((method: Method) => patchMiddleware(router, method)); -} - -/** - * Patches the prototype of Express.Router to accumulate the resolved route - * if a layer instance's `match` function was called and it returned a successful match. - * - * @see https://github.com/expressjs/express/blob/master/lib/router/index.js - * - * @param appOrRouter the router instance which can either be an app (i.e. top-level) or a (nested) router. - */ -function instrumentRouter(appOrRouter: ExpressRouter): void { - // This is how we can distinguish between app and routers - const isApp = 'settings' in appOrRouter; - - // In case the app's top-level router hasn't been initialized yet, we have to do it now - if (isApp && appOrRouter._router === undefined && appOrRouter.lazyrouter) { - appOrRouter.lazyrouter(); - } - - const router = isApp ? appOrRouter._router : appOrRouter; - - if (!router) { - /* - If we end up here, this means likely that this integration is used with Express 3 or Express 5. - For now, we don't support these versions (3 is very old and 5 is still in beta). To support Express 5, - we'd need to make more changes to the routing instrumentation because the router is no longer part of - the Express core package but maintained in its own package. The new router has different function - signatures and works slightly differently, demanding more changes than just taking the router from - `app.router` instead of `app._router`. - @see https://github.com/pillarjs/router - - TODO: Proper Express 5 support - */ - DEBUG_BUILD && logger.debug('Cannot instrument router for URL Parameterization (did not find a valid router).'); - DEBUG_BUILD && logger.debug('Routing instrumentation is currently only supported in Express 4.'); - return; - } - - const routerProto = Object.getPrototypeOf(router) as ExpressRouter; - - const originalProcessParams = routerProto.process_params; - routerProto.process_params = function process_params( - layer: Layer, - called: unknown, - req: PatchedRequest, - res: ExpressResponse & SentryTracingResponse, - done: () => unknown, - ) { - // Base case: We're in the first part of the URL (thus we start with the root '/') - if (!req._reconstructedRoute) { - req._reconstructedRoute = ''; - } - - // If the layer's partial route has params, is a regex or an array, the route is stored in layer.route. - const { layerRoutePath, isRegex, isArray, numExtraSegments }: LayerRoutePathInfo = getLayerRoutePathInfo(layer); - - if (layerRoutePath || isRegex || isArray) { - req._hasParameters = true; - } - - // Otherwise, the hardcoded path (i.e. a partial route without params) is stored in layer.path - let partialRoute; - - if (layerRoutePath) { - partialRoute = layerRoutePath; - } else { - /** - * prevent duplicate segment in _reconstructedRoute param if router match multiple routes before final path - * example: - * original url: /api/v1/1234 - * prevent: /api/api/v1/:userId - * router structure - * /api -> middleware - * /api/v1 -> middleware - * /1234 -> endpoint with param :userId - * final _reconstructedRoute is /api/v1/:userId - */ - partialRoute = preventDuplicateSegments(req.originalUrl, req._reconstructedRoute, layer.path) || ''; - } - - // Normalize the partial route so that it doesn't contain leading or trailing slashes - // and exclude empty or '*' wildcard routes. - // The exclusion of '*' routes is our best effort to not "pollute" the transaction name - // with interim handlers (e.g. ones that check authentication or do other middleware stuff). - // We want to end up with the parameterized URL of the incoming request without any extraneous path segments. - const finalPartialRoute = partialRoute - .split('/') - .filter(segment => segment.length > 0 && (isRegex || isArray || !segment.includes('*'))) - .join('/'); - - // If we found a valid partial URL, we append it to the reconstructed route - if (finalPartialRoute && finalPartialRoute.length > 0) { - // If the partial route is from a regex route, we append a '/' to close the regex - req._reconstructedRoute += `/${finalPartialRoute}${isRegex ? '/' : ''}`; - } - - // Now we check if we are in the "last" part of the route. We determine this by comparing the - // number of URL segments from the original URL to that of our reconstructed parameterized URL. - // If we've reached our final destination, we update the transaction name. - const urlLength = getNumberOfUrlSegments(stripUrlQueryAndFragment(req.originalUrl || '')) + numExtraSegments; - const routeLength = getNumberOfUrlSegments(req._reconstructedRoute); - - if (urlLength === routeLength) { - if (!req._hasParameters) { - if (req._reconstructedRoute !== req.originalUrl) { - req._reconstructedRoute = req.originalUrl ? stripUrlQueryAndFragment(req.originalUrl) : req.originalUrl; - } - } - - const transaction = res.__sentry_transaction; - const attributes = (transaction && spanToJSON(transaction).data) || {}; - if (transaction && attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'url') { - // If the request URL is '/' or empty, the reconstructed route will be empty. - // Therefore, we fall back to setting the final route to '/' in this case. - const finalRoute = req._reconstructedRoute || '/'; - - const [name, source] = extractPathForTransaction(req, { path: true, method: true, customRoute: finalRoute }); - transaction.updateName(name); - transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); - } - } - - return originalProcessParams.call(this, layer, called, req, res, done); - }; -} - -type LayerRoutePathInfo = { - layerRoutePath?: string; - isRegex: boolean; - isArray: boolean; - numExtraSegments: number; -}; - -/** - * Recreate layer.route.path from layer.regexp and layer.keys. - * Works until express.js used package path-to-regexp@0.1.7 - * or until layer.keys contain offset attribute - * - * @param layer the layer to extract the stringified route from - * - * @returns string in layer.route.path structure 'router/:pathParam' or undefined - */ -export const extractOriginalRoute = ( - path?: Layer['path'], - regexp?: Layer['regexp'], - keys?: Layer['keys'], -): string | undefined => { - if (!path || !regexp || !keys || Object.keys(keys).length === 0 || !keys[0]?.offset) { - return undefined; - } - - const orderedKeys = keys.sort((a, b) => a.offset - b.offset); - - // add d flag for getting indices from regexp result - // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- regexp comes from express.js - const pathRegex = new RegExp(regexp, `${regexp.flags}d`); - /** - * use custom type cause of TS error with missing indices in RegExpExecArray - */ - const execResult = pathRegex.exec(path) as (RegExpExecArray & { indices: [number, number][] }) | null; - - if (!execResult || !execResult.indices) { - return undefined; - } - /** - * remove first match from regex cause contain whole layer.path - */ - const [, ...paramIndices] = execResult.indices; - - if (paramIndices.length !== orderedKeys.length) { - return undefined; - } - let resultPath = path; - let indexShift = 0; - - /** - * iterate param matches from regexp.exec - */ - paramIndices.forEach((item: [number, number] | undefined, index: number) => { - /** check if offsets is define because in some cases regex d flag returns undefined */ - if (item) { - const [startOffset, endOffset] = item; - /** - * isolate part before param - */ - const substr1 = resultPath.substring(0, startOffset - indexShift); - /** - * define paramName as replacement in format :pathParam - */ - const replacement = `:${orderedKeys[index].name}`; - - /** - * isolate part after param - */ - const substr2 = resultPath.substring(endOffset - indexShift); - - /** - * recreate original path but with param replacement - */ - resultPath = substr1 + replacement + substr2; - - /** - * calculate new index shift after resultPath was modified - */ - indexShift = indexShift + (endOffset - startOffset - replacement.length); - } - }); - - return resultPath; -}; - -/** - * Extracts and stringifies the layer's route which can either be a string with parameters (`users/:id`), - * a RegEx (`/test/`) or an array of strings and regexes (`['/path1', /\/path[2-5]/, /path/:id]`). Additionally - * returns extra information about the route, such as if the route is defined as regex or as an array. - * - * @param layer the layer to extract the stringified route from - * - * @returns an object containing the stringified route, a flag determining if the route was a regex - * and the number of extra segments to the matched path that are additionally in the route, - * if the route was an array (defaults to 0). - */ -function getLayerRoutePathInfo(layer: Layer): LayerRoutePathInfo { - let lrp = layer.route?.path; - - const isRegex = isRegExp(lrp); - const isArray = Array.isArray(lrp); - - if (!lrp) { - // parse node.js major version - // Next.js will complain if we directly use `proces.versions` here because of edge runtime. - const [major] = (GLOBAL_OBJ as unknown as NodeJS.Global).process.versions.node.split('.').map(Number); - - // allow call extractOriginalRoute only if node version support Regex d flag, node 16+ - if (major >= 16) { - /** - * If lrp does not exist try to recreate original layer path from route regexp - */ - lrp = extractOriginalRoute(layer.path, layer.regexp, layer.keys); - } - } - - if (!lrp) { - return { isRegex, isArray, numExtraSegments: 0 }; - } - - const numExtraSegments = isArray - ? Math.max(getNumberOfArrayUrlSegments(lrp as RouteType[]) - getNumberOfUrlSegments(layer.path || ''), 0) - : 0; - - const layerRoutePath = getLayerRoutePathString(isArray, lrp); - - return { layerRoutePath, isRegex, isArray, numExtraSegments }; -} - -/** - * Returns the number of URL segments in an array of routes - * - * Example: ['/api/test', /\/api\/post[0-9]/, '/users/:id/details`] -> 7 - */ -function getNumberOfArrayUrlSegments(routesArray: RouteType[]): number { - return routesArray.reduce((accNumSegments: number, currentRoute: RouteType) => { - // array members can be a RegEx -> convert them toString - return accNumSegments + getNumberOfUrlSegments(currentRoute.toString()); - }, 0); -} - -/** - * Extracts and returns the stringified version of the layers route path - * Handles route arrays (by joining the paths together) as well as RegExp and normal - * string values (in the latter case the toString conversion is technically unnecessary but - * it doesn't hurt us either). - */ -function getLayerRoutePathString(isArray: boolean, lrp?: RouteType | RouteType[]): string | undefined { - if (isArray) { - return (lrp as RouteType[]).map(r => r.toString()).join(','); - } - return lrp && lrp.toString(); -} - -/** - * remove duplicate segment contain in layerPath against reconstructedRoute, - * and return only unique segment that can be added into reconstructedRoute - */ -export function preventDuplicateSegments( - originalUrl?: string, - reconstructedRoute?: string, - layerPath?: string, -): string | undefined { - // filter query params - const normalizeURL = stripUrlQueryAndFragment(originalUrl || ''); - const originalUrlSplit = normalizeURL?.split('/').filter(v => !!v); - let tempCounter = 0; - const currentOffset = reconstructedRoute?.split('/').filter(v => !!v).length || 0; - const result = layerPath - ?.split('/') - .filter(segment => { - if (originalUrlSplit?.[currentOffset + tempCounter] === segment) { - tempCounter += 1; - return true; - } - return false; - }) - .join('/'); - return result; -} diff --git a/packages/tracing-internal/src/node/integrations/graphql.ts b/packages/tracing-internal/src/node/integrations/graphql.ts deleted file mode 100644 index e61010e5953c..000000000000 --- a/packages/tracing-internal/src/node/integrations/graphql.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core'; -import { fill, loadModule, logger } from '@sentry/utils'; - -import { DEBUG_BUILD } from '../../common/debug-build'; -import type { LazyLoadedIntegration } from './lazy'; - -type GraphQLModule = { - [method: string]: (...args: unknown[]) => unknown; -}; - -/** Tracing integration for graphql package */ -export class GraphQL implements LazyLoadedIntegration { - /** - * @inheritDoc - */ - public static id: string = 'GraphQL'; - - /** - * @inheritDoc - */ - public name: string; - - private _module?: GraphQLModule; - - public constructor() { - this.name = GraphQL.id; - } - - /** @inheritdoc */ - public loadDependency(): GraphQLModule | undefined { - return (this._module = this._module || loadModule('graphql/execution/execute.js')); - } - - /** - * @inheritDoc - */ - public setupOnce(): void { - const pkg = this.loadDependency(); - - if (!pkg) { - DEBUG_BUILD && logger.error('GraphQL Integration was unable to require graphql/execution package.'); - return; - } - - fill(pkg, 'execute', function (orig: () => void | Promise) { - return function (this: unknown, ...args: unknown[]) { - return startSpan( - { - onlyIfParent: true, - name: 'execute', - op: 'graphql.execute', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.graphql.graphql', - }, - }, - () => { - return orig.call(this, ...args); - }, - ); - }; - }); - } -} diff --git a/packages/tracing-internal/src/node/integrations/index.ts b/packages/tracing-internal/src/node/integrations/index.ts deleted file mode 100644 index 0b69f4440f3a..000000000000 --- a/packages/tracing-internal/src/node/integrations/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { Express } from './express'; -export { Postgres } from './postgres'; -export { Mysql } from './mysql'; -export { Mongo } from './mongo'; -export { Prisma } from './prisma'; -export { GraphQL } from './graphql'; -export { Apollo } from './apollo'; -export * from './lazy'; diff --git a/packages/tracing-internal/src/node/integrations/lazy.ts b/packages/tracing-internal/src/node/integrations/lazy.ts deleted file mode 100644 index 635f5b082bee..000000000000 --- a/packages/tracing-internal/src/node/integrations/lazy.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { Integration, IntegrationClass } from '@sentry/types'; -import { dynamicRequire } from '@sentry/utils'; - -export interface LazyLoadedIntegration extends Integration { - /** - * Loads the integration's dependency and caches it so it doesn't have to be loaded again. - * - * If this returns undefined, the dependency could not be loaded. - */ - loadDependency(): T | undefined; -} - -export const lazyLoadedNodePerformanceMonitoringIntegrations: (() => LazyLoadedIntegration)[] = [ - () => { - const integration = dynamicRequire(module, './apollo') as { - Apollo: IntegrationClass; - }; - return new integration.Apollo(); - }, - () => { - const integration = dynamicRequire(module, './apollo') as { - Apollo: IntegrationClass; - }; - return new integration.Apollo({ useNestjs: true }); - }, - () => { - const integration = dynamicRequire(module, './graphql') as { - GraphQL: IntegrationClass; - }; - return new integration.GraphQL(); - }, - () => { - const integration = dynamicRequire(module, './mongo') as { - Mongo: IntegrationClass; - }; - return new integration.Mongo(); - }, - () => { - const integration = dynamicRequire(module, './mongo') as { - Mongo: IntegrationClass; - }; - return new integration.Mongo({ mongoose: true }); - }, - () => { - const integration = dynamicRequire(module, './mysql') as { - Mysql: IntegrationClass; - }; - return new integration.Mysql(); - }, - () => { - const integration = dynamicRequire(module, './postgres') as { - Postgres: IntegrationClass; - }; - return new integration.Postgres(); - }, -]; diff --git a/packages/tracing-internal/src/node/integrations/mongo.ts b/packages/tracing-internal/src/node/integrations/mongo.ts deleted file mode 100644 index 3cfd0fc9459e..000000000000 --- a/packages/tracing-internal/src/node/integrations/mongo.ts +++ /dev/null @@ -1,272 +0,0 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; -import { getClient } from '@sentry/core'; -import type { SpanAttributes, StartSpanOptions } from '@sentry/types'; -import { fill, isThenable, loadModule, logger } from '@sentry/utils'; - -import { DEBUG_BUILD } from '../../common/debug-build'; -import type { LazyLoadedIntegration } from './lazy'; - -// This allows us to use the same array for both defaults options and the type itself. -// (note `as const` at the end to make it a union of string literal types (i.e. "a" | "b" | ... ) -// and not just a string[]) -type Operation = (typeof OPERATIONS)[number]; -const OPERATIONS = [ - 'aggregate', // aggregate(pipeline, options, callback) - 'bulkWrite', // bulkWrite(operations, options, callback) - 'countDocuments', // countDocuments(query, options, callback) - 'createIndex', // createIndex(fieldOrSpec, options, callback) - 'createIndexes', // createIndexes(indexSpecs, options, callback) - 'deleteMany', // deleteMany(filter, options, callback) - 'deleteOne', // deleteOne(filter, options, callback) - 'distinct', // distinct(key, query, options, callback) - 'drop', // drop(options, callback) - 'dropIndex', // dropIndex(indexName, options, callback) - 'dropIndexes', // dropIndexes(options, callback) - 'estimatedDocumentCount', // estimatedDocumentCount(options, callback) - 'find', // find(query, options, callback) - 'findOne', // findOne(query, options, callback) - 'findOneAndDelete', // findOneAndDelete(filter, options, callback) - 'findOneAndReplace', // findOneAndReplace(filter, replacement, options, callback) - 'findOneAndUpdate', // findOneAndUpdate(filter, update, options, callback) - 'indexes', // indexes(options, callback) - 'indexExists', // indexExists(indexes, options, callback) - 'indexInformation', // indexInformation(options, callback) - 'initializeOrderedBulkOp', // initializeOrderedBulkOp(options, callback) - 'insertMany', // insertMany(docs, options, callback) - 'insertOne', // insertOne(doc, options, callback) - 'isCapped', // isCapped(options, callback) - 'mapReduce', // mapReduce(map, reduce, options, callback) - 'options', // options(options, callback) - 'parallelCollectionScan', // parallelCollectionScan(options, callback) - 'rename', // rename(newName, options, callback) - 'replaceOne', // replaceOne(filter, doc, options, callback) - 'stats', // stats(options, callback) - 'updateMany', // updateMany(filter, update, options, callback) - 'updateOne', // updateOne(filter, update, options, callback) -] as const; - -// All of the operations above take `options` and `callback` as their final parameters, but some of them -// take additional parameters as well. For those operations, this is a map of -// { : [] }, as a way to know what to call the operation's -// positional arguments when we add them to the span's `data` object later -const OPERATION_SIGNATURES: { - [op in Operation]?: string[]; -} = { - // aggregate intentionally not included because `pipeline` arguments are too complex to serialize well - // see https://github.com/getsentry/sentry-javascript/pull/3102 - bulkWrite: ['operations'], - countDocuments: ['query'], - createIndex: ['fieldOrSpec'], - createIndexes: ['indexSpecs'], - deleteMany: ['filter'], - deleteOne: ['filter'], - distinct: ['key', 'query'], - dropIndex: ['indexName'], - find: ['query'], - findOne: ['query'], - findOneAndDelete: ['filter'], - findOneAndReplace: ['filter', 'replacement'], - findOneAndUpdate: ['filter', 'update'], - indexExists: ['indexes'], - insertMany: ['docs'], - insertOne: ['doc'], - mapReduce: ['map', 'reduce'], - rename: ['newName'], - replaceOne: ['filter', 'doc'], - updateMany: ['filter', 'update'], - updateOne: ['filter', 'update'], -}; - -interface MongoCollection { - collectionName: string; - dbName: string; - namespace: string; - prototype: { - [operation in Operation]: (...args: unknown[]) => unknown; - }; -} - -interface MongoOptions { - operations?: Operation[]; - describeOperations?: boolean | Operation[]; - useMongoose?: boolean; -} - -interface MongoCursor { - once(event: 'close', listener: () => void): void; -} - -function isCursor(maybeCursor: MongoCursor): maybeCursor is MongoCursor { - return maybeCursor && typeof maybeCursor === 'object' && maybeCursor.once && typeof maybeCursor.once === 'function'; -} - -type MongoModule = { Collection: MongoCollection }; - -/** Tracing integration for mongo package */ -export class Mongo implements LazyLoadedIntegration { - /** - * @inheritDoc - */ - public static id: string = 'Mongo'; - - /** - * @inheritDoc - */ - public name: string; - - private _operations: Operation[]; - private _describeOperations?: boolean | Operation[]; - private _useMongoose: boolean; - - private _module?: MongoModule; - - /** - * @inheritDoc - */ - public constructor(options: MongoOptions = {}) { - this.name = Mongo.id; - this._operations = Array.isArray(options.operations) ? options.operations : (OPERATIONS as unknown as Operation[]); - this._describeOperations = 'describeOperations' in options ? options.describeOperations : true; - this._useMongoose = !!options.useMongoose; - } - - /** @inheritdoc */ - public loadDependency(): MongoModule | undefined { - const moduleName = this._useMongoose ? 'mongoose' : 'mongodb'; - return (this._module = this._module || loadModule(moduleName)); - } - - /** - * @inheritDoc - */ - public setupOnce(): void { - const pkg = this.loadDependency(); - - if (!pkg) { - const moduleName = this._useMongoose ? 'mongoose' : 'mongodb'; - DEBUG_BUILD && logger.error(`Mongo Integration was unable to require \`${moduleName}\` package.`); - return; - } - - this._instrumentOperations(pkg.Collection, this._operations); - } - - /** - * Patches original collection methods - */ - private _instrumentOperations(collection: MongoCollection, operations: Operation[]): void { - operations.forEach((operation: Operation) => this._patchOperation(collection, operation)); - } - - /** - * Patches original collection to utilize our tracing functionality - */ - private _patchOperation(collection: MongoCollection, operation: Operation): void { - if (!(operation in collection.prototype)) return; - - const getSpanContext = this._getSpanContextFromOperationArguments.bind(this); - - fill(collection.prototype, operation, function (orig: () => void | Promise) { - return function (this: unknown, ...args: unknown[]) { - const lastArg = args[args.length - 1]; - - const client = getClient(); - - const sendDefaultPii = client?.getOptions().sendDefaultPii; - - // Check if the operation was passed a callback. (mapReduce requires a different check, as - // its (non-callback) arguments can also be functions.) - if (typeof lastArg !== 'function' || (operation === 'mapReduce' && args.length === 2)) { - const span = startInactiveSpan(getSpanContext(this, operation, args, sendDefaultPii)); - const maybePromiseOrCursor = orig.call(this, ...args); - - if (isThenable(maybePromiseOrCursor)) { - return maybePromiseOrCursor.then((res: unknown) => { - span?.end(); - return res; - }); - } - // If the operation returns a Cursor - // we need to attach a listener to it to finish the span when the cursor is closed. - else if (isCursor(maybePromiseOrCursor)) { - const cursor = maybePromiseOrCursor as MongoCursor; - - try { - cursor.once('close', () => { - span?.end(); - }); - } catch (e) { - // If the cursor is already closed, `once` will throw an error. In that case, we can - // finish the span immediately. - span?.end(); - } - - return cursor; - } else { - span?.end(); - return maybePromiseOrCursor; - } - } - - const span = startInactiveSpan(getSpanContext(this, operation, args.slice(0, -1))); - - return orig.call(this, ...args.slice(0, -1), function (err: Error, result: unknown) { - span?.end(); - lastArg(err, result); - }); - }; - }); - } - - /** - * Form a SentrySpanArguments based on the user input to a given operation. - */ - private _getSpanContextFromOperationArguments( - collection: MongoCollection, - operation: Operation, - args: unknown[], - sendDefaultPii: boolean | undefined = false, - ): StartSpanOptions { - const attributes: SpanAttributes = { - 'db.system': 'mongodb', - 'db.name': collection.dbName, - 'db.operation': operation, - 'db.mongodb.collection': collection.collectionName, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `${collection.collectionName}.${operation}`, - }; - const spanContext: StartSpanOptions = { - op: 'db', - name: operation, - attributes, - onlyIfParent: true, - }; - - // If the operation takes no arguments besides `options` and `callback`, or if argument - // collection is disabled for this operation, just return early. - const signature = OPERATION_SIGNATURES[operation]; - const shouldDescribe = Array.isArray(this._describeOperations) - ? this._describeOperations.includes(operation) - : this._describeOperations; - - if (!signature || !shouldDescribe || !sendDefaultPii) { - return spanContext; - } - - try { - // Special case for `mapReduce`, as the only one accepting functions as arguments. - if (operation === 'mapReduce') { - const [map, reduce] = args as { name?: string }[]; - attributes[signature[0]] = typeof map === 'string' ? map : map.name || ''; - attributes[signature[1]] = typeof reduce === 'string' ? reduce : reduce.name || ''; - } else { - for (let i = 0; i < signature.length; i++) { - attributes[`db.mongodb.${signature[i]}`] = JSON.stringify(args[i]); - } - } - } catch (_oO) { - // no-empty - } - - return spanContext; - } -} diff --git a/packages/tracing-internal/src/node/integrations/mysql.ts b/packages/tracing-internal/src/node/integrations/mysql.ts deleted file mode 100644 index c62a6db0028f..000000000000 --- a/packages/tracing-internal/src/node/integrations/mysql.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; -import type { Span } from '@sentry/types'; -import { fill, loadModule, logger } from '@sentry/utils'; - -import { DEBUG_BUILD } from '../../common/debug-build'; -import type { LazyLoadedIntegration } from './lazy'; - -interface MysqlConnection { - prototype: { - connect: () => void; - }; - createQuery: () => void; -} - -interface MysqlConnectionConfig { - host: string; - port: number; - user: string; -} - -/** Tracing integration for node-mysql package */ -export class Mysql implements LazyLoadedIntegration { - /** - * @inheritDoc - */ - public static id: string = 'Mysql'; - - /** - * @inheritDoc - */ - public name: string; - - private _module?: MysqlConnection; - - public constructor() { - this.name = Mysql.id; - } - - /** @inheritdoc */ - public loadDependency(): MysqlConnection | undefined { - return (this._module = this._module || loadModule('mysql/lib/Connection.js')); - } - - /** - * @inheritDoc - */ - public setupOnce(): void { - const pkg = this.loadDependency(); - - if (!pkg) { - DEBUG_BUILD && logger.error('Mysql Integration was unable to require `mysql` package.'); - return; - } - - let mySqlConfig: MysqlConnectionConfig | undefined = undefined; - - try { - pkg.prototype.connect = new Proxy(pkg.prototype.connect, { - apply(wrappingTarget, thisArg: { config: MysqlConnectionConfig }, args) { - if (!mySqlConfig) { - mySqlConfig = thisArg.config; - } - return wrappingTarget.apply(thisArg, args); - }, - }); - } catch (e) { - DEBUG_BUILD && logger.error('Mysql Integration was unable to instrument `mysql` config.'); - } - - function spanDataFromConfig(): Record { - if (!mySqlConfig) { - return {}; - } - return { - 'server.address': mySqlConfig.host, - 'server.port': mySqlConfig.port, - 'db.user': mySqlConfig.user, - }; - } - - function finishSpan(span: Span | undefined): void { - if (!span) { - return; - } - - const data = spanDataFromConfig(); - Object.keys(data).forEach(key => { - span.setAttribute(key, data[key]); - }); - - span.end(); - } - - // The original function will have one of these signatures: - // function (callback) => void - // function (options, callback) => void - // function (options, values, callback) => void - fill(pkg, 'createQuery', function (orig: () => void) { - return function (this: unknown, options: unknown, values: unknown, callback: unknown) { - const span = startInactiveSpan({ - onlyIfParent: true, - name: typeof options === 'string' ? options : (options as { sql: string }).sql, - op: 'db', - attributes: { - 'db.system': 'mysql', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.mysql', - }, - }); - - if (typeof callback === 'function') { - return orig.call(this, options, values, function (err: Error, result: unknown, fields: unknown) { - finishSpan(span); - callback(err, result, fields); - }); - } - - if (typeof values === 'function') { - return orig.call(this, options, function (err: Error, result: unknown, fields: unknown) { - finishSpan(span); - values(err, result, fields); - }); - } - - // streaming, no callback! - const query = orig.call(this, options, values) as { on: (event: string, callback: () => void) => void }; - - query.on('end', () => { - finishSpan(span); - }); - - return query; - }; - }); - } -} diff --git a/packages/tracing-internal/src/node/integrations/postgres.ts b/packages/tracing-internal/src/node/integrations/postgres.ts deleted file mode 100644 index 30f94e41998c..000000000000 --- a/packages/tracing-internal/src/node/integrations/postgres.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; -import type { SpanAttributes } from '@sentry/types'; -import { fill, isThenable, loadModule, logger } from '@sentry/utils'; - -import { DEBUG_BUILD } from '../../common/debug-build'; -import type { LazyLoadedIntegration } from './lazy'; - -type PgClientQuery = ( - config: unknown, - values?: unknown, - callback?: (err: unknown, result: unknown) => void, -) => void | Promise; - -interface PgClient { - prototype: { - query: PgClientQuery; - }; -} - -interface PgClientThis { - database?: string; - host?: string; - port?: number; - user?: string; -} - -interface PgOptions { - usePgNative?: boolean; - /** - * Supply your postgres module directly, instead of having Sentry attempt automatic resolution. - * Use this if you (a) use a module that's not `pg`, or (b) use a bundler that breaks resolution (e.g. esbuild). - * - * Usage: - * ``` - * import pg from 'pg'; - * - * Sentry.init({ - * integrations: [new Sentry.Integrations.Postgres({ module: pg })], - * }); - * ``` - */ - module?: PGModule; -} - -type PGModule = { Client: PgClient; native: { Client: PgClient } | null }; - -/** Tracing integration for node-postgres package */ -export class Postgres implements LazyLoadedIntegration { - /** - * @inheritDoc - */ - public static id: string = 'Postgres'; - - /** - * @inheritDoc - */ - public name: string; - - private _usePgNative: boolean; - - private _module?: PGModule; - - public constructor(options: PgOptions = {}) { - this.name = Postgres.id; - this._usePgNative = !!options.usePgNative; - this._module = options.module; - } - - /** @inheritdoc */ - public loadDependency(): PGModule | undefined { - return (this._module = this._module || loadModule('pg')); - } - - /** - * @inheritDoc - */ - public setupOnce(): void { - const pkg = this.loadDependency(); - - if (!pkg) { - DEBUG_BUILD && logger.error('Postgres Integration was unable to require `pg` package.'); - return; - } - - const Client = this._usePgNative ? pkg.native?.Client : pkg.Client; - - if (!Client) { - DEBUG_BUILD && logger.error("Postgres Integration was unable to access 'pg-native' bindings."); - return; - } - - /** - * function (query, callback) => void - * function (query, params, callback) => void - * function (query) => Promise - * function (query, params) => Promise - * function (pg.Cursor) => pg.Cursor - */ - fill(Client.prototype, 'query', function (orig: PgClientQuery) { - return function (this: PgClientThis, config: unknown, values: unknown, callback: unknown) { - const attributes: SpanAttributes = { - 'db.system': 'postgresql', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.postgres', - }; - - try { - if (this.database) { - attributes['db.name'] = this.database; - } - if (this.host) { - attributes['server.address'] = this.host; - } - if (this.port) { - attributes['server.port'] = this.port; - } - if (this.user) { - attributes['db.user'] = this.user; - } - } catch (e) { - // ignore - } - - const span = startInactiveSpan({ - onlyIfParent: true, - name: typeof config === 'string' ? config : (config as { text: string }).text, - op: 'db', - attributes, - }); - - if (typeof callback === 'function') { - return orig.call(this, config, values, function (err: Error, result: unknown) { - span?.end(); - callback(err, result); - }); - } - - if (typeof values === 'function') { - return orig.call(this, config, function (err: Error, result: unknown) { - span?.end(); - values(err, result); - }); - } - - const rv = typeof values !== 'undefined' ? orig.call(this, config, values) : orig.call(this, config); - - if (isThenable(rv)) { - return rv.then((res: unknown) => { - span?.end(); - return res; - }); - } - - span?.end(); - return rv; - }; - }); - } -} diff --git a/packages/tracing-internal/src/node/integrations/prisma.ts b/packages/tracing-internal/src/node/integrations/prisma.ts deleted file mode 100644 index cb9db58e2a21..000000000000 --- a/packages/tracing-internal/src/node/integrations/prisma.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core'; -import type { Integration } from '@sentry/types'; -import { addNonEnumerableProperty, logger } from '@sentry/utils'; - -import { DEBUG_BUILD } from '../../common/debug-build'; - -type PrismaAction = - | 'findUnique' - | 'findMany' - | 'findFirst' - | 'create' - | 'createMany' - | 'update' - | 'updateMany' - | 'upsert' - | 'delete' - | 'deleteMany' - | 'executeRaw' - | 'queryRaw' - | 'aggregate' - | 'count' - | 'runCommandRaw'; - -interface PrismaMiddlewareParams { - model?: unknown; - action: PrismaAction; - args: unknown; - dataPath: string[]; - runInTransaction: boolean; -} - -type PrismaMiddleware = ( - params: PrismaMiddlewareParams, - next: (params: PrismaMiddlewareParams) => Promise, -) => Promise; - -interface PrismaClient { - _sentryInstrumented?: boolean; - _engineConfig?: { - activeProvider?: string; - clientVersion?: string; - }; - $use: (cb: PrismaMiddleware) => void; -} - -function isValidPrismaClient(possibleClient: unknown): possibleClient is PrismaClient { - return !!possibleClient && !!(possibleClient as PrismaClient)['$use']; -} - -/** Tracing integration for @prisma/client package */ -export class Prisma implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'Prisma'; - - /** - * @inheritDoc - */ - public name: string; - - /** - * @inheritDoc - */ - public constructor(options: { client?: unknown } = {}) { - this.name = Prisma.id; - - // We instrument the PrismaClient inside the constructor and not inside `setupOnce` because in some cases of server-side - // bundling (Next.js) multiple Prisma clients can be instantiated, even though users don't intend to. When instrumenting - // in setupOnce we can only ever instrument one client. - // https://github.com/getsentry/sentry-javascript/issues/7216#issuecomment-1602375012 - // In the future we might explore providing a dedicated PrismaClient middleware instead of this hack. - if (isValidPrismaClient(options.client) && !options.client._sentryInstrumented) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - addNonEnumerableProperty(options.client as any, '_sentryInstrumented', true); - - const clientData: Record = {}; - try { - const engineConfig = (options.client as PrismaClient)._engineConfig; - if (engineConfig) { - const { activeProvider, clientVersion } = engineConfig; - if (activeProvider) { - clientData['db.system'] = activeProvider; - } - if (clientVersion) { - clientData['db.prisma.version'] = clientVersion; - } - } - } catch (e) { - // ignore - } - - options.client.$use((params, next: (params: PrismaMiddlewareParams) => Promise) => { - const action = params.action; - const model = params.model; - - return startSpan( - { - name: model ? `${model} ${action}` : action, - onlyIfParent: true, - op: 'db.prisma', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.prisma', - ...clientData, - 'db.operation': action, - }, - }, - () => next(params), - ); - }); - } else { - DEBUG_BUILD && - logger.warn('Unsupported Prisma client provided to PrismaIntegration. Provided client:', options.client); - } - } - - /** - * @inheritDoc - */ - public setupOnce(): void { - // Noop - here for backwards compatibility - } -} diff --git a/packages/tracing-internal/test/node/express.test.ts b/packages/tracing-internal/test/node/express.test.ts deleted file mode 100644 index 1631971d9863..000000000000 --- a/packages/tracing-internal/test/node/express.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { extractOriginalRoute, preventDuplicateSegments } from '../../src/node/integrations/express'; - -/** - * prevent duplicate segment in _reconstructedRoute param if router match multiple routes before final path - * example: - * original url: /api/v1/1234 - * prevent: /api/api/v1/:userId - * router structure - * /api -> middleware - * /api/v1 -> middleware - * /1234 -> endpoint with param :userId - * final _reconstructedRoute is /api/v1/:userId - */ -describe('unit Test for preventDuplicateSegments', () => { - it('should return api segment', () => { - const originalUrl = '/api/v1/1234'; - const reconstructedRoute = ''; - const layerPath = '/api'; - const result = preventDuplicateSegments(originalUrl, reconstructedRoute, layerPath); - expect(result).toBe('api'); - }); - - it('should prevent duplicate segment api', () => { - const originalUrl = '/api/v1/1234'; - const reconstructedRoute = '/api'; - const layerPath = '/api/v1'; - const result = preventDuplicateSegments(originalUrl, reconstructedRoute, layerPath); - expect(result).toBe('v1'); - }); - - it('should prevent duplicate segment v1', () => { - const originalUrl = '/api/v1/1234'; - const reconstructedRoute = '/api/v1'; - const layerPath = '/v1/1234'; - const result1 = preventDuplicateSegments(originalUrl, reconstructedRoute, layerPath); - expect(result1).toBe('1234'); - }); - - it('should prevent duplicate segment v1 originalUrl with query param without trailing slash', () => { - const originalUrl = '/api/v1/1234?queryParam=123'; - const reconstructedRoute = '/api/v1'; - const layerPath = '/v1/1234'; - const result1 = preventDuplicateSegments(originalUrl, reconstructedRoute, layerPath); - expect(result1).toBe('1234'); - }); - - it('should prevent duplicate segment v1 originalUrl with query param with trailing slash', () => { - const originalUrl = '/api/v1/1234/?queryParam=123'; - const reconstructedRoute = '/api/v1'; - const layerPath = '/v1/1234'; - const result1 = preventDuplicateSegments(originalUrl, reconstructedRoute, layerPath); - expect(result1).toBe('1234'); - }); -}); -describe('preventDuplicateSegments should handle empty input gracefully', () => { - it('Empty input values', () => { - expect(preventDuplicateSegments()).toBeUndefined(); - }); - - it('Empty originalUrl', () => { - expect(preventDuplicateSegments('', '/api/v1/1234', '/api/api/v1/1234')).toBe(''); - }); - - it('Empty reconstructedRoute', () => { - expect(preventDuplicateSegments('/api/v1/1234', '', '/api/api/v1/1234')).toBe('api/v1/1234'); - }); - - it('Empty layerPath', () => { - expect(preventDuplicateSegments('/api/v1/1234', '/api/v1/1234', '')).toBe(''); - }); -}); - -// parse node.js major version -const [major] = process.versions.node.split('.').map(Number); -// Test this funciton only if node is 16+ because regex d flag is support from node 16+ -if (major >= 16) { - describe('extractOriginalRoute', () => { - it('should return undefined if path, regexp, or keys are missing', () => { - expect(extractOriginalRoute('/example')).toBeUndefined(); - expect(extractOriginalRoute('/example', /test/)).toBeUndefined(); - }); - - it('should return undefined if keys do not contain an offset property', () => { - const path = '/example'; - const regex = /example/; - const key = { name: 'param1', offset: 0, optional: false }; - expect(extractOriginalRoute(path, regex, [key])).toBeUndefined(); - }); - - it('should return the original route path when valid inputs are provided', () => { - const path = '/router/123'; - const regex = /^\/router\/(\d+)$/; - const keys = [{ name: 'pathParam', offset: 8, optional: false }]; - expect(extractOriginalRoute(path, regex, keys)).toBe('/router/:pathParam'); - }); - - it('should handle multiple parameters in the route', () => { - const path = '/user/42/profile/username'; - const regex = /^\/user\/(\d+)\/profile\/(\w+)$/; - const keys = [ - { name: 'userId', offset: 6, optional: false }, - { name: 'username', offset: 17, optional: false }, - ]; - expect(extractOriginalRoute(path, regex, keys)).toBe('/user/:userId/profile/:username'); - }); - - it('should handle complex regex scheme extract from array of routes', () => { - const path1 = '/@fs/*'; - const path2 = '/@vite/client'; - const path3 = '/@react-refresh'; - const path4 = '/manifest.json'; - - const regex = - /(?:^\/manifest\.json\/?(?=\/|$)|^\/@vite\/client\/?(?=\/|$)|^\/@react-refresh\/?(?=\/|$)|^\/src\/(.*)\/?(?=\/|$)|^\/vite\/(.*)\/?(?=\/|$)|^\/node_modules\/(.*)\/?(?=\/|$)|^\/@fs\/(.*)\/?(?=\/|$)|^\/@vite-plugin-checker-runtime\/?(?=\/|$)|^\/?$\/?(?=\/|$)|^\/home\/?$\/?(?=\/|$)|^\/login\/?(?=\/|$))/; - const keys = [ - { name: 0, offset: 8, optional: false }, - { name: 0, offset: 8, optional: false }, - { name: 0, offset: 9, optional: false }, - { name: 0, offset: 17, optional: false }, - ]; - - expect(extractOriginalRoute(path1, regex, keys)).toBe('/@fs/:0'); - expect(extractOriginalRoute(path2, regex, keys)).toBe('/@vite/client'); - expect(extractOriginalRoute(path3, regex, keys)).toBe('/@react-refresh'); - expect(extractOriginalRoute(path4, regex, keys)).toBe('/manifest.json'); - }); - }); -} diff --git a/packages/utils/src/worldwide.ts b/packages/utils/src/worldwide.ts index 6ac98ac0f6ae..9f925565d786 100644 --- a/packages/utils/src/worldwide.ts +++ b/packages/utils/src/worldwide.ts @@ -12,7 +12,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { Client, Integration, MetricsAggregator, Scope } from '@sentry/types'; +import type { Client, MetricsAggregator, Scope } from '@sentry/types'; import type { SdkSource } from './env'; @@ -20,9 +20,7 @@ import type { SdkSource } from './env'; export interface InternalGlobal { navigator?: { userAgent?: string }; console: Console; - Sentry?: { - Integrations?: Integration[]; - }; + Sentry?: any; onerror?: { (event: object | string, source?: string, lineno?: number, colno?: number, error?: Error): any; __SENTRY_INSTRUMENTED__?: true;