diff --git a/packages/core/src/integrations/express/index.ts b/packages/core/src/integrations/express/index.ts deleted file mode 100644 index 70f798ceb0f0..000000000000 --- a/packages/core/src/integrations/express/index.ts +++ /dev/null @@ -1,156 +0,0 @@ -/** - * Platform-portable Express tracing integration. - * - * @module - * - * This Sentry integration is a derivative work based on the OpenTelemetry - * Express instrumentation. - * - * - * - * Extended under the terms of the Apache 2.0 license linked below: - * - * ---- - * - * Copyright The OpenTelemetry Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// This whole module backs the deprecated Express exports (superseded by `expressIntegration()`), so it -// references its own deprecated types/functions throughout. -/* oxlint-disable typescript/no-deprecated */ - -import { debug } from '../../utils/debug-logger'; -import { DEBUG_BUILD } from '../../debug-build'; -import type { - ExpressApplication, - ExpressIntegrationOptions, - ExpressLayer, - ExpressModuleExport, - ExpressRouter, - ExpressRouterv4, - ExpressRouterv5, -} from './types'; -import { getLayerPath, isExpressWithoutRouterPrototype, isExpressWithRouterPrototype } from './utils'; -import { wrapMethod } from '../../utils/object'; -import { patchLayer } from './patch-layer'; -import { getDefaultExport } from '../../utils/get-default-export'; - -/** - * This is a portable instrumentatiton function that works in any environment - * where Express can be loaded, without depending on OpenTelemetry. - * - * @example - * ```javascript - * import express from 'express'; - * import * as Sentry from '@sentry/deno'; // or any SDK that extends core - * - * Sentry.patchExpressModule(express, () => ({})); - * ``` - * - * @deprecated Express is now instrumented automatically via `expressIntegration()`. This export is - * no longer used and will be removed in the next major version. - */ -export function patchExpressModule( - moduleExports: ExpressModuleExport, - getOptions: () => ExpressIntegrationOptions, -): ExpressModuleExport { - if (typeof getOptions !== 'function') { - throw new TypeError('`patchExpressModule(moduleExports, getOptions)` requires a `getOptions` callback'); - } - - // pass in the require() or import() result of express - const express = getDefaultExport(moduleExports); - const routerProto: ExpressRouterv4 | ExpressRouterv5 | undefined = isExpressWithRouterPrototype(express) - ? express.Router.prototype // Express v5 - : isExpressWithoutRouterPrototype(express) - ? express.Router // Express v4 - : undefined; - - if (!routerProto) { - throw new TypeError('no valid Express route function to instrument'); - } - - // oxlint-disable-next-line @typescript-eslint/unbound-method - const originalRouteMethod = routerProto.route; - try { - wrapMethod( - routerProto, - 'route', - function routeTrace(this: ExpressRouter, ...args: Parameters[]) { - const route = originalRouteMethod.apply(this, args); - const layer = this.stack[this.stack.length - 1] as ExpressLayer; - patchLayer(getOptions, layer, getLayerPath(args)); - return route; - }, - ); - } catch (e) { - DEBUG_BUILD && debug.error('Failed to patch express route method:', e); - } - - // oxlint-disable-next-line @typescript-eslint/unbound-method - const originalRouterUse = routerProto.use; - try { - wrapMethod( - routerProto, - 'use', - function useTrace(this: ExpressApplication, ...args: Parameters) { - const route = originalRouterUse.apply(this, args); - const layer = this.stack[this.stack.length - 1]; - if (!layer) { - return route; - } - patchLayer(getOptions, layer, getLayerPath(args)); - return route; - }, - ); - } catch (e) { - DEBUG_BUILD && debug.error('Failed to patch express use method:', e); - } - - const { application } = express; - const originalApplicationUse = application.use; - try { - wrapMethod( - application, - 'use', - function appUseTrace( - this: ExpressApplication & { - _router?: ExpressRouter; - router?: ExpressRouter; - }, - ...args: Parameters - ) { - // If we access app.router in express 4.x we trigger an assertion error. - // This property existed in v3, was removed in v4 and then re-added in v5. - const route = originalApplicationUse.apply(this, args); - const router = isExpressWithRouterPrototype(express) ? this.router : this._router; - if (router) { - const layer = router.stack[router.stack.length - 1]; - if (layer) { - patchLayer(getOptions, layer, getLayerPath(args)); - } - } - return route; - }, - ); - } catch (e) { - DEBUG_BUILD && debug.error('Failed to patch express application.use method:', e); - } - - return express; -} - -// The deprecated `expressErrorHandler` / `setupExpressErrorHandler` now live in `@sentry/server-utils` -// (alongside the channel-based `expressIntegration()`), so they are not defined here anymore. diff --git a/packages/core/src/integrations/express/patch-layer.ts b/packages/core/src/integrations/express/patch-layer.ts deleted file mode 100644 index 7c23c2e11654..000000000000 --- a/packages/core/src/integrations/express/patch-layer.ts +++ /dev/null @@ -1,347 +0,0 @@ -/** - * Platform-portable Express tracing integration. - * - * @module - * - * This Sentry integration is a derivative work based on the OpenTelemetry - * Express instrumentation. - * - * - * - * Extended under the terms of the Apache 2.0 license linked below: - * - * ---- - * - * Copyright The OpenTelemetry Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// This module backs the deprecated Express exports (superseded by `expressIntegration()`), so it -// references the deprecated `ExpressIntegrationOptions` type. -/* oxlint-disable typescript/no-deprecated */ - -import { - HTTP_METHOD, - HTTP_REQUEST_METHOD, - HTTP_ROUTE, - SENTRY_OP, - SENTRY_SEGMENT_NAME_SOURCE, -} from '@sentry/conventions/attributes'; -import { HANDLER, MIDDLEWARE, ROUTER } from '@sentry/conventions/op'; -import { DEBUG_BUILD } from '../../debug-build'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes'; -import { SPAN_STATUS_ERROR, withActiveSpan } from '../../tracing'; -import { hasSpanStreamingEnabled } from '../../tracing/spans/hasSpanStreamingEnabled'; -import { REQUEST_HANDLER_SPAN_NAME_FALLBACK, ROUTER_SPAN_NAME_FALLBACK } from '../../tracing/spans/spanNames'; -import { startSpanManual } from '../../tracing/trace'; -import { debug } from '../../utils/debug-logger'; -import type { SpanAttributes } from '../../types/span'; -import { getActiveSpan, getRootSpan, spanToJSON } from '../../utils/spanUtils'; -import { getStoredLayers, storeLayer } from './request-layer-store'; -import { - type ExpressRequest, - type ExpressResponse, - type ExpressIntegrationOptions, - type ExpressLayer, - ATTR_HTTP_ROUTE, - ATTR_EXPRESS_TYPE, - ATTR_EXPRESS_NAME, - ExpressLayerType_ROUTER, - ExpressLayerType_MIDDLEWARE, - ExpressLayerType_REQUEST_HANDLER, -} from './types'; -import { - asErrorAndMessage, - getActualMatchedRoute, - getConstructedRoute, - getLayerMetadata, - isLayerIgnored, -} from './utils'; -import { getClient, getIsolationScope } from '../../currentScopes'; -import { getDefaultIsolationScope } from '../../defaultScopes'; -import { getOriginalFunction, markFunctionWrapped } from '../../utils/object'; -import { setSDKProcessingMetadata } from './set-sdk-processing-metadata'; - -const EXPRESS_TYPE_TO_SPAN_OP: Record = { - [ExpressLayerType_MIDDLEWARE]: MIDDLEWARE, - [ExpressLayerType_REQUEST_HANDLER]: HANDLER, - [ExpressLayerType_ROUTER]: ROUTER, -}; - -export type ExpressPatchLayerOptions = Pick< - ExpressIntegrationOptions, - 'onRouteResolved' | 'ignoreLayers' | 'ignoreLayersType' ->; - -export function patchLayer( - getOptions: () => ExpressPatchLayerOptions, - maybeLayer?: ExpressLayer, - layerPath?: string, -): void { - if (!maybeLayer?.handle) { - return; - } - const layer = maybeLayer; - - const layerHandleOriginal = layer.handle; - - // avoid patching multiple times the same layer - if (getOriginalFunction(layerHandleOriginal)) { - return; - } - - if (layerHandleOriginal.length === 4) { - // todo: instrument error handlers - return; - } - - function layerHandlePatched( - this: ExpressLayer, - req: ExpressRequest, - res: ExpressResponse, - //oxlint-disable-next-line no-explicit-any - ...otherArgs: any[] - ) { - const options = getOptions(); - - // Set normalizedRequest here because expressRequestHandler middleware - // (registered via setupExpressErrorHandler) is added after routes and - // therefore never runs for successful requests — route handlers typically - // send a response without calling next(). It would be safe to set this - // multiple times, since the data is identical, but more performant not to. - setSDKProcessingMetadata(req); - - // Only create spans when there's an active parent span - // Without a parent span, this request is being ignored, so skip it - const parentSpan = getActiveSpan(); - if (!parentSpan) { - return layerHandleOriginal.apply(this, [req, res, ...otherArgs]); - } - - if (layerPath) { - storeLayer(req, layerPath); - } - const storedLayers = getStoredLayers(req); - const isLayerPathStored = !!layerPath; - - const constructedRoute = getConstructedRoute(req); - const actualMatchedRoute = getActualMatchedRoute(req, constructedRoute); - - options.onRouteResolved?.(actualMatchedRoute); - - const metadata = getLayerMetadata(constructedRoute, layer, layerPath); - const name = metadata.attributes[ATTR_EXPRESS_NAME]; - const type = metadata.attributes[ATTR_EXPRESS_TYPE]; - const attributes: SpanAttributes = Object.assign(metadata.attributes, { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.express', - [SENTRY_OP]: EXPRESS_TYPE_TO_SPAN_OP[type], - }); - if (actualMatchedRoute) { - attributes[ATTR_HTTP_ROUTE] = actualMatchedRoute; - } - - // Propagate the route to the root `http.server` span before the ignore check, so the span is still - // named when the layer's own span is ignored. Runs for every layer that matched a route, not just - // request handlers: mounted middleware (`app.use('/trpc', ...)`) resolves a route too. - if (actualMatchedRoute) { - applyRouteToRootSpan(actualMatchedRoute); - } - - // verify against the config if the layer should be ignored - if (isLayerIgnored(metadata.attributes[ATTR_EXPRESS_NAME], type, options)) { - // XXX: the isLayerPathStored guard here is *not* present in the - // original @opentelemetry/instrumentation-express impl, but was - // suggested by the Sentry code review bot. It appears to correctly - // prevent improper layer calculation in the case where there's a - // middleware without a layerPath argument. It's unclear whether - // that's possible, or if any existing code depends on that "bug". - if (isLayerPathStored) { - storedLayers.pop(); - } - return layerHandleOriginal.apply(this, [req, res, ...otherArgs]); - } - - const currentScope = getIsolationScope(); - if (currentScope !== getDefaultIsolationScope()) { - if (type === 'request_handler') { - // type cast b/c Otel unfortunately types info.request as any :( - const method = req.method ? req.method.toUpperCase() : 'GET'; - currentScope.setTransactionName(`${method} ${constructedRoute}`); - } - } else { - DEBUG_BUILD && debug.warn('Isolation scope is still default isolation scope - skipping setting transactionName'); - } - - const client = getClient(); - // With span streaming, span names have to be low cardinality, so router - // and request handler spans are named after their route. A route that did - // not validate against the request URL can describe a different request, - // so those spans take the static fallback instead. - const isStreamedSpan = !!client && hasSpanStreamingEnabled(client); - const isStreamedRouterSpan = isStreamedSpan && type === ExpressLayerType_ROUTER; - const isStreamedRequestHandlerSpan = isStreamedSpan && type === ExpressLayerType_REQUEST_HANDLER; - - const spanName = isStreamedRouterSpan - ? actualMatchedRoute || ROUTER_SPAN_NAME_FALLBACK - : isStreamedRequestHandlerSpan - ? actualMatchedRoute || REQUEST_HANDLER_SPAN_NAME_FALLBACK - : name; - - return startSpanManual({ name: spanName, attributes }, span => { - let spanHasEnded = false; - // TODO: Fix router spans (getRouterPath does not work properly) to - // have useful names before removing this branch - if (metadata.attributes[ATTR_EXPRESS_TYPE] === ExpressLayerType_ROUTER) { - span.end(); - spanHasEnded = true; - } - // listener for response.on('finish') - const onResponseFinish = () => { - if (!spanHasEnded) { - spanHasEnded = true; - span.end(); - } - }; - - // verify we have a callback - for (let i = 0; i < otherArgs.length; i++) { - const callback = otherArgs[i] as Function; - if (typeof callback !== 'function') { - continue; - } - - //oxlint-disable-next-line no-explicit-any - otherArgs[i] = function (...args: any[]) { - // express considers anything but an empty value, "route" or "router" - // passed to its callback to be an error - const maybeError = args[0]; - const isError = !!maybeError && maybeError !== 'route' && maybeError !== 'router'; - if (!spanHasEnded && isError) { - const [_, message] = asErrorAndMessage(maybeError); - // intentionally do not record the exception here, because - // the error handler we assign does that, provided the user - // correctly calls setupExpressErrorHandler. - // TODO: A future enhancement can automatically attach - // the error handler if we detect that it has not been added. - span.setStatus({ - code: SPAN_STATUS_ERROR, - message, - }); - } - - if (!spanHasEnded) { - spanHasEnded = true; - res.removeListener('finish', onResponseFinish); - span.end(); - } - if (!(req.route && isError) && isLayerPathStored) { - storedLayers.pop(); - } - // execute the callback back in the parent's scope, so that - // we bubble up each level as next() is called. - return withActiveSpan(parentSpan, () => callback.apply(this, args)); - }; - break; - } - - try { - return layerHandleOriginal.apply(this, [req, res, ...otherArgs]); - } catch (anyError) { - const [_, message] = asErrorAndMessage(anyError); - // intentionally do not record the exception here, because - // the error handler we assign does that, provided the user - // correctly calls setupExpressErrorHandler. - // TODO: A future enhancement can automatically attach - // the error handler if we detect that it has not been added. - span.setStatus({ - code: SPAN_STATUS_ERROR, - message, - }); - throw anyError; - /* v8 ignore next - it sees the block end at the throw */ - } finally { - // At this point if the callback wasn't called, that means - // either the layer is asynchronous (so it will call the - // callback later on) or that the layer directly ends the - // http response, so we'll hook into the "finish" event to - // handle the later case. - if (!spanHasEnded) { - res.once('finish', onResponseFinish); - } - } - }); - } - - // `handle` isn't just a regular function in some cases. It also contains - // some properties holding metadata and state so we need to proxy them - // through through patched function. Use a for-in to also pick up properties - // that other libraries might add to the prototype before we instrument. - // ref: https://github.com/open-telemetry/opentelemetry-js-contrib/issues/1950 - // ref: https://github.com/open-telemetry/opentelemetry-js-contrib/issues/2271 - // oxlint-disable-next-line guard-for-in - for (const key in layerHandleOriginal as Function & Record) { - // skip standard function prototype fields that both have - if (key in layerHandlePatched) { - continue; - } - Object.defineProperty(layerHandlePatched, key, { - get() { - return layerHandleOriginal[key]; - }, - set(value) { - layerHandleOriginal[key] = value; - }, - }); - } - - markFunctionWrapped(layerHandlePatched, layerHandleOriginal); - - Object.defineProperty(layer, 'handle', { - enumerable: true, - configurable: true, - writable: true, - value: layerHandlePatched, - }); -} - -/** - * Write the resolved route onto the root `http.server` span. - * - * With span streaming the root span starts out named after the request method only, because no route - * is known at that point. Unlike the Node SDK — which goes through `setHttpServerSpanRouteAttribute` — - * nothing else on this path renames it, so a routed request would otherwise keep the method-only name. - */ -function applyRouteToRootSpan(route: string): void { - const client = getClient(); - if (!client || !hasSpanStreamingEnabled(client)) { - return; - } - - const activeSpan = getActiveSpan(); - const rootSpan = activeSpan && getRootSpan(activeSpan); - if (!rootSpan) { - return; - } - - const attributes = spanToJSON(rootSpan).attributes; - if (attributes[SENTRY_OP] !== 'http.server') { - return; - } - - // eslint-disable-next-line typescript/no-deprecated - const method = attributes[HTTP_REQUEST_METHOD] || attributes[HTTP_METHOD] || 'GET'; - rootSpan.updateName(`${method} ${route}`); - rootSpan.setAttribute(HTTP_ROUTE, route); - rootSpan.setAttribute(SENTRY_SEGMENT_NAME_SOURCE, 'route'); -} diff --git a/packages/core/src/integrations/express/request-layer-store.ts b/packages/core/src/integrations/express/request-layer-store.ts deleted file mode 100644 index 3408c2405e1b..000000000000 --- a/packages/core/src/integrations/express/request-layer-store.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Platform-portable Express tracing integration. - * - * @module - * - * This Sentry integration is a derivative work based on the OpenTelemetry - * Express instrumentation. - * - * - * - * Extended under the terms of the Apache 2.0 license linked below: - * - * ---- - * - * Copyright The OpenTelemetry Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { ExpressRequest } from './types'; - -// map of patched request objects to stored layers -const requestLayerStore = new WeakMap(); -export const storeLayer = (req: ExpressRequest, layer: string) => { - const store = requestLayerStore.get(req); - if (!store) { - requestLayerStore.set(req, [layer]); - } else { - store.push(layer); - } -}; - -export const getStoredLayers = (req: ExpressRequest) => { - let store = requestLayerStore.get(req); - if (!store) { - store = []; - requestLayerStore.set(req, store); - } - return store; -}; diff --git a/packages/core/src/integrations/express/set-sdk-processing-metadata.ts b/packages/core/src/integrations/express/set-sdk-processing-metadata.ts deleted file mode 100644 index 0b694a40a360..000000000000 --- a/packages/core/src/integrations/express/set-sdk-processing-metadata.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Platform-portable Express tracing integration. - * - * @module - * - * This Sentry integration is a derivative work based on the OpenTelemetry - * Express instrumentation. - * - * - * - * Extended under the terms of the Apache 2.0 license linked below: - * - * ---- - * - * Copyright The OpenTelemetry Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Abstract this out because we call it in multiple places, and it's cheaper to - * only do one time for any given request. - */ - -import type { ExpressRequest } from './types'; -import { getIsolationScope } from '../../currentScopes'; -import { httpRequestToRequestData } from '../../utils/request'; - -// TODO: consider moving this into a core util, eg -// setSDKProcessingMetadataFromRequest(..), if other integrations need it. -export function setSDKProcessingMetadata(request: ExpressRequest) { - const sdkProcMeta = getIsolationScope()?.getScopeData()?.sdkProcessingMetadata; - if (!sdkProcMeta?.normalizedRequest) { - const normalizedRequest = httpRequestToRequestData(request); - getIsolationScope().setSDKProcessingMetadata({ normalizedRequest }); - } -} diff --git a/packages/core/src/integrations/express/types.ts b/packages/core/src/integrations/express/types.ts deleted file mode 100644 index 5bf33e578ad0..000000000000 --- a/packages/core/src/integrations/express/types.ts +++ /dev/null @@ -1,189 +0,0 @@ -/** - * Platform-portable Express tracing integration. - * - * @module - * - * This Sentry integration is a derivative work based on the OpenTelemetry - * Express instrumentation. - * - * - * - * Extended under the terms of the Apache 2.0 license linked below: - * - * ---- - * - * Copyright The OpenTelemetry Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { RequestEventData } from '../../types/request'; -import type { SpanAttributes } from '../../types/span'; - -export const ATTR_EXPRESS_NAME = 'express.name'; -export const ATTR_HTTP_ROUTE = 'http.route'; -export const ATTR_EXPRESS_TYPE = 'express.type'; - -export type ExpressExport = { - Router: ExpressRouterv5 | ExpressRouterv4; - application: ExpressApplication; -}; - -export type ExpressExportv5 = ExpressExport & { - Router: ExpressRouterv5; -}; - -export type ExpressExportv4 = ExpressExport & { - Router: ExpressRouterv4; -}; - -export type ExpressModuleExport = ExpressExport | { default: ExpressExport }; - -export interface ExpressRequest extends RequestEventData { - originalUrl: string; - route: unknown; - // Note: req.res is typed as optional (only present after middleware init). - // mark optional to preserve compat with express v4 types. - res?: ExpressResponse; -} - -// just a minimum type def for what we need, since this also needs to -// work in environments lacking node:http -export interface ExpressResponse { - once(ev: string, listener: Function): this; - removeListener(ev: string, listener?: Function): this; - emit(ev: string, ...data: unknown[]): this; -} - -export interface NextFunction { - (err?: unknown): void; - /** - * "Break-out" of a router by calling {next('router')}; - * @see {https://expressjs.com/en/guide/using-middleware.html#middleware.router} - */ - (deferToNext: 'router'): void; - /** - * "Break-out" of a route by calling {next('route')}; - * @see {https://expressjs.com/en/guide/using-middleware.html#middleware.application} - */ - (deferToNext: 'route'): void; -} - -// Need to mark this as `any` so they don't conflict with the actual express -//oxlint-disable-next-line no-explicit-any -export type ExpressApplicationRequestHandler = (...handlers: any[]) => any; - -export type ExpressRequestInfo = { - /** An express request object */ - request: T; - route: string; - layerType: ExpressLayerType; -}; - -export type ExpressLayerType = 'router' | 'middleware' | 'request_handler'; -export const ExpressLayerType_ROUTER = 'router'; -export const ExpressLayerType_MIDDLEWARE = 'middleware'; -export const ExpressLayerType_REQUEST_HANDLER = 'request_handler'; - -export type PathParams = string | RegExp | Array; -export type LayerPathSegment = string | RegExp | number; - -export interface ExpressRoute { - path: string; - stack: ExpressLayer[]; -} - -export type ExpressRouterv4 = ExpressRouter; - -export interface ExpressRouterv5 { - prototype: ExpressRouter; -} - -// https://github.com/expressjs/express/blob/main/lib/router/layer.js#L33 -export type ExpressLayer = { - handle: Function & - Record & { - stack?: ExpressLayer[]; - }; - name: string; - params: { [key: string]: string }; - path?: string; - regexp: RegExp; - route?: ExpressLayer; -}; - -export type ExpressRouter = { - params: { [key: string]: string }; - _params: string[]; - caseSensitive: boolean; - mergeParams: boolean; - strict: boolean; - stack: ExpressLayer[]; - route(prefix: PathParams): ExpressRoute; - use(...handlers: unknown[]): unknown; -}; - -export type IgnoreMatcher = string | RegExp | ((name: string) => boolean); - -/** - * @deprecated The core Express integration is superseded by `expressIntegration()`. This type is - * deprecated and will be removed in the next major version. - */ -export type ExpressIntegrationOptions = { - /** Ignore specific based on their name */ - ignoreLayers?: IgnoreMatcher[]; - /** Ignore specific layers based on their type */ - ignoreLayersType?: ExpressLayerType[]; - /** - * Optional callback invoked each time a layer resolves the matched HTTP route. - * Platform-specific integrations (e.g. Node.js) use this to propagate the - * resolved route to the underlying transport layer (e.g. OTel RPCMetadata). - */ - onRouteResolved?: (route: string | undefined) => void; -}; - -export type LayerMetadata = { - attributes: SpanAttributes; - name: string; -}; - -export interface ExpressApplication { - stack: ExpressLayer[]; - use: ExpressApplicationRequestHandler; -} - -export interface MiddlewareError extends Error { - status?: number | string; - statusCode?: number | string; - status_code?: number | string; - output?: { - statusCode?: number | string; - }; -} - -/** - * @deprecated `expressIntegration()` captures errors automatically. This type is deprecated and will - * be removed in the next major version. - */ -export type ExpressMiddleware = (req: ExpressRequest, res: ExpressResponse, next: () => void) => void; - -/** - * @deprecated `expressIntegration()` captures errors automatically. This type is deprecated and will - * be removed in the next major version. - */ -export type ExpressErrorMiddleware = ( - error: MiddlewareError, - req: ExpressRequest, - res: ExpressResponse, - next: (error: MiddlewareError) => void, -) => void; diff --git a/packages/core/src/integrations/express/utils.ts b/packages/core/src/integrations/express/utils.ts deleted file mode 100644 index 80dc13af7c34..000000000000 --- a/packages/core/src/integrations/express/utils.ts +++ /dev/null @@ -1,269 +0,0 @@ -/** - * Platform-portable Express tracing integration. - * - * @module - * - * This Sentry integration is a derivative work based on the OpenTelemetry - * Express instrumentation. - * - * - * - * Extended under the terms of the Apache 2.0 license linked below: - * - * ---- - * - * Copyright The OpenTelemetry Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// This module backs the deprecated Express exports (superseded by `expressIntegration()`), so it -// references the deprecated `ExpressIntegrationOptions` type. -/* oxlint-disable typescript/no-deprecated */ - -import type { SpanAttributes } from '../../types/span'; -import { getStoredLayers } from './request-layer-store'; -import type { - ExpressIntegrationOptions, - ExpressLayer, - ExpressLayerType, - ExpressRequest, - LayerPathSegment, - MiddlewareError, - ExpressRouterv4, - ExpressExportv5, - ExpressExportv4, -} from './types'; -import { - ATTR_EXPRESS_NAME, - ATTR_EXPRESS_TYPE, - ExpressLayerType_MIDDLEWARE, - ExpressLayerType_REQUEST_HANDLER, - ExpressLayerType_ROUTER, -} from './types'; -import { stringMatchesSomePattern } from '../../utils/string'; - -/** - * Converts a user-provided error value into an error and error message pair - * - * @param error - User-provided error value - * @returns Both an Error or string representation of the value and an error message - */ -export const asErrorAndMessage = (error: unknown): [string | Error, string] => - error instanceof Error ? [error, error.message] : [String(error), String(error)]; - -/** - * Checks if a route contains parameter patterns (e.g., :id, :userId) - * which are valid even if they don't exactly match the original URL - */ -export function isRoutePattern(route: string): boolean { - return route.includes(':') || route.includes('*'); -} - -/** - * Parse express layer context to retrieve a name and attributes. - * @param route The route of the layer - * @param layer Express layer - * @param [layerPath] if present, the path on which the layer has been mounted - */ -export const getLayerMetadata = ( - route: string, - layer: ExpressLayer, - layerPath?: string, -): { - attributes: SpanAttributes & { [ATTR_EXPRESS_NAME]: string; [ATTR_EXPRESS_TYPE]: ExpressLayerType }; - name: string; -} => { - if (layer.name === 'router') { - const maybeRouterPath = getRouterPath('', layer); - const extractedRouterPath = maybeRouterPath ? maybeRouterPath : layerPath || route || '/'; - - return { - attributes: { - [ATTR_EXPRESS_NAME]: extractedRouterPath, - [ATTR_EXPRESS_TYPE]: ExpressLayerType_ROUTER, - }, - name: `router - ${extractedRouterPath}`, - }; - } else if (layer.name === 'bound dispatch' || layer.name === 'handle') { - return { - attributes: { - [ATTR_EXPRESS_NAME]: (route || layerPath) ?? 'request handler', - [ATTR_EXPRESS_TYPE]: ExpressLayerType_REQUEST_HANDLER, - }, - name: `request handler${layer.path ? ` - ${route || layerPath}` : ''}`, - }; - } else { - return { - attributes: { - [ATTR_EXPRESS_NAME]: layer.name, - [ATTR_EXPRESS_TYPE]: ExpressLayerType_MIDDLEWARE, - }, - name: `middleware - ${layer.name}`, - }; - } -}; - -/** - * Recursively search the router path from layer stack - * @param path The path to reconstruct - * @param layer The layer to reconstruct from - * @returns The reconstructed path - */ -export const getRouterPath = (path: string, layer: ExpressLayer): string => { - const stackLayer = Array.isArray(layer.handle?.stack) ? layer.handle?.stack?.[0] : undefined; - - if (stackLayer?.route?.path) { - return `${path}${stackLayer.route.path}`; - } - - if (stackLayer && Array.isArray(stackLayer?.handle?.stack)) { - return getRouterPath(path, stackLayer); - } - - return path; -}; - -/** - * Check whether the given request is ignored by configuration - * It will not re-throw exceptions from `list` provided by the client - * @param constant e.g URL of request - * @param [list] List of ignore patterns - * @param [onException] callback for doing something when an exception has - * occurred - */ -export type ExpressIsLayerIgnoredOptions = Pick; -export const isLayerIgnored = ( - name: string, - type: ExpressLayerType, - config?: ExpressIsLayerIgnoredOptions, -): boolean => { - if (Array.isArray(config?.ignoreLayersType) && config?.ignoreLayersType?.includes(type)) { - return true; - } - if (!Array.isArray(config?.ignoreLayers)) { - return false; - } - try { - return stringMatchesSomePattern(name, config.ignoreLayers, true); - } catch {} - - return false; -}; - -/** - * Extracts the actual matched route from Express request for OpenTelemetry instrumentation. - * Returns the route that should be used as the http.route attribute. - * - * @param req - The Express request object with layers store - * @param constructedRoute - The constructed route from `getConstructedRoute` - * @returns The matched route string or undefined if no valid route is found - */ -export function getActualMatchedRoute(req: ExpressRequest, constructedRoute: string): string | undefined { - const layersStore = getStoredLayers(req); - - // If no layers are stored, no route can be determined - if (layersStore.length === 0) { - return undefined; - } - - // Handle root path case - if all paths are root, only return root if originalUrl is also root - // The layer store also includes root paths in case a non-existing url was requested - if (layersStore.every(path => path === '/')) { - return req.originalUrl === '/' ? '/' : undefined; - } - - if (constructedRoute === '*') { - return constructedRoute; - } - - // For RegExp routes or route arrays, return the constructed route - // This handles the case where the route is defined using RegExp or an array - if ( - constructedRoute.includes('/') && - (constructedRoute.includes(',') || - constructedRoute.includes('\\') || - constructedRoute.includes('*') || - constructedRoute.includes('[')) - ) { - return constructedRoute; - } - - // Ensure route starts with '/' if it doesn't already - const normalizedRoute = constructedRoute.startsWith('/') ? constructedRoute : `/${constructedRoute}`; - - // Validate that this appears to be a matched route - // A route is considered matched if: - // 1. We have a constructed route - // 2. The original URL matches or starts with our route pattern - const isValidRoute = - normalizedRoute.length > 0 && - (req.originalUrl === normalizedRoute || - req.originalUrl.startsWith(normalizedRoute) || - isRoutePattern(normalizedRoute)); - - return isValidRoute ? normalizedRoute : undefined; -} - -export function getConstructedRoute(req: ExpressRequest) { - const layersStore: string[] = getStoredLayers(req); - - let constructedRoute: string = ''; - for (const path of layersStore) { - if (path === '/' || path === '/*') { - continue; - } - constructedRoute += !constructedRoute || constructedRoute.endsWith('/') ? path : `/${path}`; - } - - return constructedRoute.replace(/\/{2,}/g, '/'); -} - -export const getLayerPath = (args: unknown[]): string | undefined => { - const firstArg = args[0]; - - if (Array.isArray(firstArg)) { - return firstArg.map(arg => extractLayerPathSegment(arg) || '').join(','); - } - - return extractLayerPathSegment(firstArg as LayerPathSegment); -}; - -const extractLayerPathSegment = (arg: LayerPathSegment): string | undefined => - typeof arg === 'string' ? arg : arg instanceof RegExp || typeof arg === 'number' ? String(arg) : undefined; - -// v5 we instrument Router.prototype -// v4 we instrument Router itself -export const isExpressWithRouterPrototype = (express: unknown): express is ExpressExportv5 => - isExpressRouterPrototype((express as ExpressExportv5)?.Router?.prototype); - -// In Express v4, Router is a function (not a plain object), so we need to accept both -const isExpressRouterPrototype = (routerProto?: unknown): routerProto is ExpressRouterv4 => - (typeof routerProto === 'object' || typeof routerProto === 'function') && - !!routerProto && - 'route' in routerProto && - typeof (routerProto as ExpressRouterv4).route === 'function'; - -export const isExpressWithoutRouterPrototype = (express: unknown): express is ExpressExportv4 => - isExpressRouterPrototype((express as ExpressExportv4).Router) && !isExpressWithRouterPrototype(express); - -function getStatusCodeFromResponse(error: MiddlewareError): number { - const statusCode = error.status || error.statusCode || error.status_code || error.output?.statusCode; - return statusCode ? parseInt(statusCode as string, 10) : 500; -} - -/** Returns true if response code is internal server error */ -export function defaultShouldHandleError(error: MiddlewareError): boolean { - const status = getStatusCodeFromResponse(error); - return status >= 500; -} diff --git a/packages/core/src/server.ts b/packages/core/src/server.ts index 06320cc59b38..25dac9eeb7a4 100644 --- a/packages/core/src/server.ts +++ b/packages/core/src/server.ts @@ -15,10 +15,6 @@ export { vercelWaitUntil } from './utils/vercelWaitUntil'; export { flushIfServerless } from './utils/flushIfServerless'; export { callFrameToStackFrame, watchdogTimer } from './utils/anr'; export { safeUnref as _INTERNAL_safeUnref } from './utils/timer'; -/* oxlint-disable typescript/no-deprecated -- deprecated Express exports, kept until the next major */ -export { patchExpressModule } from './integrations/express/index'; -export type { ExpressMiddleware, ExpressErrorMiddleware } from './integrations/express/types'; -/* oxlint-enable typescript/no-deprecated */ export { patchHttpModuleClient } from './integrations/http/client-patch'; export { getHttpClientSubscriptions } from './integrations/http/client-subscriptions'; export { getHttpServerSubscriptions, isStaticAssetRequest } from './integrations/http/server-subscription'; diff --git a/packages/core/test/lib/integrations/express/index.test.ts b/packages/core/test/lib/integrations/express/index.test.ts deleted file mode 100644 index 78fe8b8f2648..000000000000 --- a/packages/core/test/lib/integrations/express/index.test.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { patchExpressModule } from '../../../../src/integrations/express/index'; - -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import type { Mock } from 'vitest'; -import type { - ExpressIntegrationOptions, - ExpressExportv5, - ExpressExportv4, - ExpressLayer, - ExpressModuleExport, - ExpressRoute, - ExpressRouterv4, - ExpressRouterv5, -} from '../../../../src/integrations/express/types'; -import type { WrappedFunction } from '../../../../src/types/wrappedfunction'; - -vi.mock('../../../../src/debug-build', () => ({ - DEBUG_BUILD: true, -})); -const debugErrors: [string, Error][] = []; -vi.mock('../../../../src/utils/debug-logger', () => ({ - debug: { - warn: () => {}, - error: (msg: string, er: Error) => { - debugErrors.push([msg, er]); - }, - }, -})); - -beforeEach(() => (patchLayerCalls.length = 0)); -const patchLayerCalls: [getOptions: () => ExpressIntegrationOptions, layer: ExpressLayer, layerPath?: string][] = []; - -vi.mock('../../../../src/integrations/express/patch-layer', () => ({ - patchLayer: (getOptions: () => ExpressIntegrationOptions, layer?: ExpressLayer, layerPath?: string) => { - if (layer) { - patchLayerCalls.push([getOptions, layer, layerPath]); - } - }, -})); - -type ExpressSpies = Record<'routerUse' | 'routerRoute' | 'appUse', Mock<() => void>>; - -// get a fresh copy of a mock Express version 4 export -function getExpress4(): ExpressExportv4 & { spies: ExpressSpies } { - const routerRoute = vi.fn(); - const routerUse = vi.fn(); - const appUse = vi.fn(); - const spies = { - routerRoute, - routerUse, - appUse, - } as const; - const express = Object.assign(function express() {}, { - spies, - application: { use: appUse }, - Router: Object.assign(function Router() {}, { - route: routerRoute, - use: routerUse, - stack: [{ name: 'layer0' }, { name: 'layer1' }, { name: 'layerFinal' }], - }), - }) as unknown as ExpressExportv4 & { spies: ExpressSpies }; - Object.assign(express.application, { _router: express.Router }); - - return express; -} - -// get a fresh copy of a mock Express version 5 export -function getExpress5(): ExpressExportv5 & { spies: ExpressSpies } { - const routerRoute = vi.fn(); - const routerUse = vi.fn(); - const appUse = vi.fn(); - const spies = { - routerRoute, - routerUse, - appUse, - } as const; - const expressv5 = Object.assign(function express() {}, { - spies, - application: { use: appUse }, - Router: class Router { - stack: ExpressLayer[] = []; - route(...args: unknown[]) { - return routerRoute(...args); - } - use(...args: unknown[]) { - return routerUse(...args); - } - }, - }) as unknown as ExpressExportv5 & { spies: ExpressSpies }; - const stack = [{ name: 'layer0' }, { name: 'layer1' }, { name: 'layerFinal' }]; - Object.assign(expressv5.application, { - router: { stack }, - }); - - return expressv5; -} - -describe('patchExpressModule', () => { - it('throws trying to patch the wrong thing', () => { - expect(() => { - patchExpressModule({} as unknown as ExpressModuleExport, () => ({})); - }).toThrowError('no valid Express route function to instrument'); - }); - - it('throws trying to patch without a getOptions getter', () => { - const express = getExpress4(); - expect(() => { - //@ts-expect-error The type error prevents this, by design - patchExpressModule(express); - }).toThrowError('`patchExpressModule(moduleExports, getOptions)` requires a `getOptions` callback'); - }); - - it('can patch expressv4 style module', () => { - for (const useDefault of [false, true]) { - const express = getExpress4(); - const moduleExports = useDefault ? { default: express } : express; - const r = express.Router as ExpressRouterv4; - const a = express.application; - expect((r.use as WrappedFunction).__sentry_original__).toBe(undefined); - expect((r.route as WrappedFunction).__sentry_original__).toBe(undefined); - expect((a.use as WrappedFunction).__sentry_original__).toBe(undefined); - - patchExpressModule(moduleExports, () => ({})); - - expect(typeof (r.use as WrappedFunction).__sentry_original__).toBe('function'); - expect(typeof (r.route as WrappedFunction).__sentry_original__).toBe('function'); - expect(typeof (a.use as WrappedFunction).__sentry_original__).toBe('function'); - } - }); - - it('can patch expressv5 style module', () => { - for (const useDefault of [false, true]) { - const express = getExpress5(); - const r = express.Router as ExpressRouterv5; - const a = express.application; - const moduleExports = useDefault ? { default: express } : express; - expect((r.prototype.use as WrappedFunction).__sentry_original__).toBe(undefined); - expect((r.prototype.route as WrappedFunction).__sentry_original__).toBe(undefined); - expect((a.use as WrappedFunction).__sentry_original__).toBe(undefined); - - patchExpressModule(moduleExports, () => ({})); - - expect(typeof (r.prototype.use as WrappedFunction).__sentry_original__).toBe('function'); - expect(typeof (r.prototype.route as WrappedFunction).__sentry_original__).toBe('function'); - expect(typeof (a.use as WrappedFunction).__sentry_original__).toBe('function'); - } - }); - - it('calls patched and original Router.route', () => { - const expressv4 = getExpress4(); - const { spies } = expressv4; - const getOptions = () => ({}); - patchExpressModule(expressv4, getOptions); - expressv4.Router.route('a'); - expect(spies.routerRoute).toHaveBeenCalledExactlyOnceWith('a'); - }); - - it('calls patched and original Router.use', () => { - const expressv4 = getExpress4(); - const { spies } = expressv4; - const getOptions = () => ({}); - patchExpressModule(expressv4, getOptions); - expressv4.Router.use('a'); - expect(patchLayerCalls).toStrictEqual([[getOptions, { name: 'layerFinal' }, 'a']]); - expect(spies.routerUse).toHaveBeenCalledExactlyOnceWith('a'); - }); - - it('skips patchLayer call in Router.use if no layer in the stack', () => { - const expressv4 = getExpress4(); - const { spies } = expressv4; - const getOptions = () => ({}); - patchExpressModule(expressv4, getOptions); - const { stack } = expressv4.Router; - expressv4.Router.stack = []; - expressv4.Router.use('a'); - expressv4.Router.stack = stack; - expect(patchLayerCalls).toStrictEqual([]); - expect(spies.routerUse).toHaveBeenCalledExactlyOnceWith('a'); - }); - - it('calls patched and original application.use', () => { - const expressv4 = getExpress4(); - const { spies } = expressv4; - const getOptions = () => ({}); - patchExpressModule(expressv4, getOptions); - expressv4.application.use('a'); - expect(patchLayerCalls).toStrictEqual([[getOptions, { name: 'layerFinal' }, 'a']]); - expect(spies.appUse).toHaveBeenCalledExactlyOnceWith('a'); - }); - - it('calls patched and original application.use on express v5', () => { - const expressv5 = getExpress5(); - const { spies } = expressv5; - const getOptions = () => ({}); - patchExpressModule(expressv5, getOptions); - expressv5.application.use('a'); - expect(patchLayerCalls).toStrictEqual([[getOptions, { name: 'layerFinal' }, 'a']]); - expect(spies.appUse).toHaveBeenCalledExactlyOnceWith('a'); - }); - - it('skips patchLayer on application.use if no router found', () => { - const expressv4 = getExpress4(); - const { spies } = expressv4; - const getOptions = () => ({}); - patchExpressModule(expressv4, getOptions); - const app = expressv4.application as { - _router?: ExpressRoute; - }; - const { _router } = app; - delete app._router; - expressv4.application.use('a'); - app._router = _router; - // no router, so no layers to patch! - expect(patchLayerCalls).toStrictEqual([]); - expect(spies.appUse).toHaveBeenCalledExactlyOnceWith('a'); - }); - - it('debug error when patching fails', () => { - const expressv5 = getExpress5(); - const getOptions = () => ({}); - patchExpressModule(expressv5, getOptions); - patchExpressModule(expressv5, getOptions); - expect(debugErrors).toStrictEqual([ - ['Failed to patch express route method:', new Error('Attempting to wrap method route multiple times')], - ['Failed to patch express use method:', new Error('Attempting to wrap method use multiple times')], - ['Failed to patch express application.use method:', new Error('Attempting to wrap method use multiple times')], - ]); - }); -}); diff --git a/packages/core/test/lib/integrations/express/patch-layer.test.ts b/packages/core/test/lib/integrations/express/patch-layer.test.ts deleted file mode 100644 index 926ac08040b5..000000000000 --- a/packages/core/test/lib/integrations/express/patch-layer.test.ts +++ /dev/null @@ -1,907 +0,0 @@ -import { describe, beforeEach, it, expect, vi } from 'vitest'; -import { type ExpressPatchLayerOptions, patchLayer } from '../../../../src/integrations/express/patch-layer'; -import { - type ExpressRequest, - type ExpressLayer, - type ExpressResponse, -} from '../../../../src/integrations/express/types'; -import { getStoredLayers, storeLayer } from '../../../../src/integrations/express/request-layer-store'; -import { type StartSpanOptions } from '../../../../src/types/startSpanOptions'; -import { type Span } from '../../../../src/types/span'; -import { EventEmitter } from 'node:events'; -import { getOriginalFunction, markFunctionWrapped } from '../../../../src'; - -// must be var to hoist above vi.mock -var DEBUG_BUILD = true; -beforeEach(() => (DEBUG_BUILD = true)); -vi.mock('../../../../src/debug-build', () => ({ - get DEBUG_BUILD() { - return DEBUG_BUILD ?? true; - }, -})); - -const warnings: string[] = []; -beforeEach(() => (warnings.length = 0)); -vi.mock('../../../../src/utils/debug-logger', () => ({ - debug: { - warn(msg: string) { - warnings.push(msg); - }, - }, -})); - -let inDefaultIsolationScope = false; -beforeEach(() => (inDefaultIsolationScope = false)); -const transactionNames: string[] = []; -const notDefaultIsolationScope = { - _scopeData: {} as { sdkProcessingMetadata?: unknown }, - getScopeData() { - return this._scopeData; - }, - setTransactionName(name: string) { - transactionNames.push(name); - }, - setSDKProcessingMetadata() {}, -}; -const defaultIsolationScope = { - _scopeData: {} as { sdkProcessingMetadata?: unknown }, - getScopeData() { - return this._scopeData; - }, - setSDKProcessingMetadata(data: unknown) { - this._scopeData.sdkProcessingMetadata = data; - }, -}; -let spanStreamingEnabled = false; -beforeEach(() => (spanStreamingEnabled = false)); -vi.mock('../../../../src/currentScopes', () => ({ - getIsolationScope() { - return inDefaultIsolationScope ? defaultIsolationScope : notDefaultIsolationScope; - }, - getClient() { - return { getOptions: () => ({ traceLifecycle: spanStreamingEnabled ? 'stream' : 'static' }) }; - }, -})); -vi.mock('../../../../src/defaultScopes', () => ({ - getDefaultIsolationScope() { - return defaultIsolationScope; - }, -})); - -const mockSpans: MockSpan[] = []; -beforeEach(() => (mockSpans.length = 0)); -beforeEach(() => (transactionNames.length = 0)); -class MockSpan { - ended = false; - status: { code: number; message: string } = { code: 0, message: 'OK' }; - attributes: Record; - name: string; - - constructor(options: StartSpanOptions) { - this.name = options.name; - this.attributes = options.attributes ?? {}; - } - - updateName(name: string) { - this.name = name; - return this; - } - - setStatus(status: { code: number; message: string }) { - this.status = status; - } - - setAttributes(o: Record) { - for (const [k, v] of Object.entries(o)) { - this.setAttribute(k, v); - } - } - - setAttribute(key: string, value: unknown) { - this.attributes[key] = value; - } - - end() { - if (this.ended) { - throw new Error('ended span multiple times!'); - } - this.ended = true; - } - getSpanJSON(): MockSpanJSON { - // not the whole thing obviously, just enough to know we called it - return { - status: this.status, - data: this.attributes, - description: this.name, - }; - } -} -type MockSpanJSON = { - status?: { code: number; message: string }; - description: string; - data: Record; -}; - -/** verify we get all the expected spans and no more */ -const checkSpans = (expectations: Partial[]) => { - for (const exp of expectations) { - const span = mockSpans.pop()?.getSpanJSON(); - expect(span).toMatchObject(exp); - } - expect(mockSpans.map(m => m.getSpanJSON())).toStrictEqual([]); -}; - -let hasActiveSpan = true; -// Stands in for the root `http.server` span so the route-to-root-span write can be asserted. -const parentSpan = { - name: 'GET', - attributes: { 'sentry.op': 'http.server' } as Record, - updateName(name: string) { - this.name = name; - return this; - }, - setAttribute(key: string, value: unknown) { - this.attributes[key] = value; - return this; - }, -}; -beforeEach(() => { - parentSpan.name = 'GET'; - parentSpan.attributes = { 'sentry.op': 'http.server' }; -}); -vi.mock('../../../../src/utils/spanUtils', async () => ({ - ...(await import('../../../../src/utils/spanUtils')), - getActiveSpan() { - return hasActiveSpan ? parentSpan : undefined; - }, - getRootSpan(span: unknown) { - return span; - }, - spanToJSON(span: { attributes?: Record }) { - return { attributes: span.attributes ?? {} }; - }, -})); - -vi.mock('../../../../src/tracing', () => ({ - SPAN_STATUS_ERROR: 2, - withActiveSpan(span: unknown, cb: Function) { - expect(span).toBe(parentSpan); - return cb(); - }, -})); - -vi.mock('../../../../src/tracing/trace', () => ({ - startSpanManual(options: StartSpanOptions, callback: (span: Span) => T): T { - const span = new MockSpan(options); - mockSpans.push(span); - return callback(span as unknown as Span); - }, -})); - -describe('patchLayer', () => { - describe('no-ops', () => { - it('if layer is missing', () => { - // mostly for coverage, verifying it doesn't throw or anything - patchLayer(() => ({})); - }); - - it('if layer.handle is missing', () => { - // mostly for coverage, verifying it doesn't throw or anything - patchLayer(() => ({}), { handle: null } as unknown as ExpressLayer); - }); - - it('if layer already patched', () => { - // mostly for coverage, verifying it doesn't throw or anything - function wrapped() {} - function original() {} - markFunctionWrapped(wrapped, original); - const layer = { - handle: wrapped, - } as unknown as ExpressLayer; - patchLayer(() => ({}), layer); - expect(layer.handle).toBe(wrapped); - }); - - it('if layer handler of length 4', () => { - // TODO: this should be expanded when we instrument error handlers - function original(_1: unknown, _2: unknown, _3: unknown, _4: unknown) {} - - const layer = { - handle: original, - } as unknown as ExpressLayer; - patchLayer(() => ({}), layer); - expect(layer.handle).toBe(original); - }); - - it('wraps the function', () => { - // mostly a gut-check that we actually do mark wrapped - function original(_1: unknown, _2: unknown, _3: unknown) {} - - const layer = { - handle: original, - } as unknown as ExpressLayer; - patchLayer(() => ({}), layer); - expect(getOriginalFunction(layer.handle)).toBe(original); - }); - }); - - it('ignores when no parent span has been started', () => { - hasActiveSpan = false; - const options: ExpressPatchLayerOptions = {}; - const req = Object.assign(new EventEmitter(), { - originalUrl: '/a/b/c', - }) as unknown as ExpressRequest; - - const layerHandleOriginal = vi.fn(); - const layer = { - name: 'mw', - handle: layerHandleOriginal, - } as unknown as ExpressLayer; - - const res = Object.assign(new EventEmitter(), {}) as unknown as ExpressResponse; - - storeLayer(req, 'a'); - storeLayer(req, '/:boo'); - storeLayer(req, '/:car'); - - patchLayer(() => options, layer); - layer.handle(req, res); - expect(layerHandleOriginal).toHaveBeenCalledOnce(); - - // should not have emitted any spans, it was ignored. - checkSpans([]); - - hasActiveSpan = true; - }); - - it('ignores layers that should be ignored, runs otherwise', () => { - const onRouteResolved = vi.fn(); - const options: ExpressPatchLayerOptions = { - onRouteResolved, - ignoreLayersType: ['middleware'], - }; - const req = Object.assign(new EventEmitter(), { - originalUrl: '/a/b/c/layerPath', - }) as unknown as ExpressRequest; - - const layerHandleOriginal = vi.fn(); - const layer = { - name: 'mw', - handle: layerHandleOriginal, - } as unknown as ExpressLayer; - - const res = Object.assign(new EventEmitter(), {}) as unknown as ExpressResponse; - - storeLayer(req, 'a'); - storeLayer(req, '/:boo'); - storeLayer(req, '/:car'); - - patchLayer(() => options, layer, '/layerPath'); - layer.handle(req, res); - expect(onRouteResolved).toHaveBeenCalledExactlyOnceWith('/a/:boo/:car/layerPath'); - expect(layerHandleOriginal).toHaveBeenCalledOnce(); - - // should not have emitted any spans, it was ignored. - checkSpans([]); - options.ignoreLayersType = []; - layer.handle(req, res); - const span = mockSpans[0]; - expect(span?.ended).toBe(false); - checkSpans([ - { - status: { code: 0, message: 'OK' }, - data: { - 'express.name': 'mw', - 'express.type': 'middleware', - 'http.route': '/a/:boo/:car/layerPath', - 'sentry.op': 'middleware', - 'sentry.origin': 'auto.http.express', - }, - description: 'mw', - }, - ]); - res.emit('finish'); - expect(span?.ended).toBe(true); - checkSpans([]); - }); - - it('pops storedLayers when ignoring router or request_handler type layers', () => { - for (const type of ['router', 'request_handler'] as const) { - const options: ExpressPatchLayerOptions = { ignoreLayersType: [type] }; - const req = Object.assign(new EventEmitter(), { - originalUrl: '/a/b/c/layerPath', - }) as unknown as ExpressRequest; - - // simulate layers already stored for previous path segments - storeLayer(req, '/a'); - storeLayer(req, '/b'); - - // patch a layer of the ignored type with a layerPath - const layerHandleOriginal = vi.fn(); - // layer.name must match what getLayerMetadata uses to classify each type: - // 'router' → router, 'bound dispatch' → request_handler, other → middleware - const layerName = type === 'router' ? 'router' : 'bound dispatch'; - const layer = { name: layerName, handle: layerHandleOriginal } as unknown as ExpressLayer; - patchLayer(() => options, layer, '/c'); - - // storeLayer('/c') happens inside the patched handle, before being popped - // after handle returns, storedLayers should be back to ['/a', '/b'] - layer.handle(req, Object.assign(new EventEmitter(), {}) as unknown as ExpressResponse); - - // the ignored layer's path must be cleaned up so subsequent layers see the correct route - expect(getStoredLayers(req)).toStrictEqual(['/a', '/b']); - } - }); - - it('warns about not setting name in default isolation scope', async () => { - inDefaultIsolationScope = true; - DEBUG_BUILD = true; - const options: ExpressPatchLayerOptions = {}; - const req = Object.assign(new EventEmitter(), { - originalUrl: '/a/b/c/layerPath', - }) as unknown as ExpressRequest; - - const layerHandleOriginal = Object.assign(vi.fn(), { - x: true, - // a field that the wrapped one will have, so we skip it. - toString() { - return 'x'; - }, - }); - const layer = { - name: 'handle', - handle: layerHandleOriginal, - } as unknown as ExpressLayer; - - const res = Object.assign(new EventEmitter(), {}) as unknown as ExpressResponse; - - storeLayer(req, 'a'); - storeLayer(req, '/:boo'); - storeLayer(req, '/:car'); - - patchLayer(() => options, layer, '/layerPath'); - expect(getOriginalFunction(layer.handle)).toBe(layerHandleOriginal); - expect(layer.handle.x).toBe(true); - layer.handle.x = false; - expect(layerHandleOriginal.x).toBe(false); - - warnings.length = 0; - layer.handle(req, res); - expect(warnings).toStrictEqual([ - 'Isolation scope is still default isolation scope - skipping setting transactionName', - ]); - expect(layerHandleOriginal).toHaveBeenCalledOnce(); - - // should not have emitted any spans, it was ignored. - checkSpans([ - { - status: { code: 0, message: 'OK' }, - data: { - 'express.name': 'a/:boo/:car/layerPath', - 'express.type': 'request_handler', - 'http.route': '/a/:boo/:car/layerPath', - 'sentry.op': 'handler', - 'sentry.origin': 'auto.http.express', - }, - description: 'a/:boo/:car/layerPath', - }, - ]); - res.emit('finish'); - checkSpans([]); - }); - - it('writes the resolved route onto the root http.server span when span streaming is enabled', () => { - // Regression guard: with streaming the root span starts named `GET`, and nothing else on this - // path renames it — a routed request would otherwise keep the method-only name. - spanStreamingEnabled = true; - - const req = Object.assign(new EventEmitter(), { - originalUrl: '/a/b/c/layerPath', - method: 'get', - }) as unknown as ExpressRequest; - const res = Object.assign(new EventEmitter(), {}) as unknown as ExpressResponse; - const layer = { name: 'handle', handle: vi.fn() } as unknown as ExpressLayer; - - storeLayer(req, 'a'); - storeLayer(req, '/:boo'); - - patchLayer(() => ({}), layer); - layer.handle(req, res); - - expect(parentSpan.name).toBe('GET /a/:boo'); - expect(parentSpan.attributes['http.route']).toBe('/a/:boo'); - expect(parentSpan.attributes['sentry.segment.name.source']).toBe('route'); - }); - - it('names the root route `GET /` rather than leaving the route empty', () => { - // `getConstructedRoute` skips `/`, so the root handler must take its route from the matched route. - spanStreamingEnabled = true; - - const req = Object.assign(new EventEmitter(), { - originalUrl: '/', - method: 'get', - }) as unknown as ExpressRequest; - const res = Object.assign(new EventEmitter(), {}) as unknown as ExpressResponse; - const layer = { name: 'handle', handle: vi.fn() } as unknown as ExpressLayer; - - storeLayer(req, '/'); - - patchLayer(() => ({}), layer); - layer.handle(req, res); - - expect(parentSpan.name).toBe('GET /'); - expect(parentSpan.attributes['http.route']).toBe('/'); - }); - - it('applies the route from mounted middleware, not only from request handlers', () => { - // `app.use('/trpc', handler)` matches a route without being a request handler. - spanStreamingEnabled = true; - - const req = Object.assign(new EventEmitter(), { - originalUrl: '/trpc/foo', - method: 'get', - }) as unknown as ExpressRequest; - const res = Object.assign(new EventEmitter(), {}) as unknown as ExpressResponse; - // A layer name other than `handle`/`bound dispatch`/`router` is treated as middleware. - const layer = { name: 'trpcMiddleware', handle: vi.fn() } as unknown as ExpressLayer; - - storeLayer(req, '/trpc'); - - patchLayer(() => ({}), layer); - layer.handle(req, res); - - expect(parentSpan.name).toBe('GET /trpc'); - expect(parentSpan.attributes['http.route']).toBe('/trpc'); - }); - - it('leaves the root span name alone without span streaming', () => { - spanStreamingEnabled = false; - - const req = Object.assign(new EventEmitter(), { - originalUrl: '/a/b/c/layerPath', - method: 'get', - }) as unknown as ExpressRequest; - const res = Object.assign(new EventEmitter(), {}) as unknown as ExpressResponse; - const layer = { name: 'handle', handle: vi.fn() } as unknown as ExpressLayer; - - storeLayer(req, 'a'); - storeLayer(req, '/:boo'); - - patchLayer(() => ({}), layer); - layer.handle(req, res); - - expect(parentSpan.name).toBe('GET'); - expect(parentSpan.attributes['http.route']).toBeUndefined(); - }); - - it('sets tx name in isolation scope', async () => { - DEBUG_BUILD = true; - expect( - (await import('../../../../src/currentScopes')).getIsolationScope() === - (await import('../../../../src/defaultScopes')).getDefaultIsolationScope(), - ).toBe(false); - - const options: ExpressPatchLayerOptions = {}; - const req = Object.assign(new EventEmitter(), { - originalUrl: '/a/b/c/layerPath', - }) as unknown as ExpressRequest; - - const layerHandleOriginal = vi.fn(); - const layer = { - name: 'handle', - handle: layerHandleOriginal, - } as unknown as ExpressLayer; - - const res = Object.assign(new EventEmitter(), {}) as unknown as ExpressResponse; - - storeLayer(req, 'a'); - storeLayer(req, '/:boo'); - storeLayer(req, '/:car'); - - patchLayer(() => options, layer); - expect(getOriginalFunction(layer.handle)).toBe(layerHandleOriginal); - warnings.length = 0; - layer.handle(req, res); - - req.method = 'put'; - layer.handle(req, res); - expect(warnings).toStrictEqual([]); - - expect(transactionNames).toStrictEqual(['GET a/:boo/:car', 'PUT a/:boo/:car']); - expect(layerHandleOriginal).toHaveBeenCalledTimes(2); - - // should not have emitted any spans, it was ignored. - checkSpans([ - { - status: { code: 0, message: 'OK' }, - data: { - 'express.name': 'a/:boo/:car', - 'express.type': 'request_handler', - 'http.route': '/a/:boo/:car', - 'sentry.op': 'handler', - 'sentry.origin': 'auto.http.express', - }, - description: 'a/:boo/:car', - }, - { - status: { code: 0, message: 'OK' }, - data: { - 'express.name': 'a/:boo/:car', - 'express.type': 'request_handler', - 'http.route': '/a/:boo/:car', - 'sentry.op': 'handler', - 'sentry.origin': 'auto.http.express', - }, - description: 'a/:boo/:car', - }, - ]); - res.emit('finish'); - checkSpans([]); - }); - - it('works with layerPath field', () => { - const onRouteResolved = vi.fn(); - const options: ExpressPatchLayerOptions = { onRouteResolved }; - const req = Object.assign(new EventEmitter(), { - originalUrl: '/a/b/c/d', - }) as unknown as ExpressRequest; - - const layerHandleOriginal = vi.fn(); - const layer = { - name: 'mw', - handle: layerHandleOriginal, - } as unknown as ExpressLayer; - - const res = Object.assign(new EventEmitter(), {}) as unknown as ExpressResponse; - - storeLayer(req, '/a'); - storeLayer(req, '/b'); - - patchLayer(() => options, layer, '/c'); - layer.handle(req, res); - expect(onRouteResolved).toHaveBeenCalledExactlyOnceWith('/a/b/c'); - const span = mockSpans[0]; - checkSpans([ - { - status: { code: 0, message: 'OK' }, - data: { - 'express.name': 'mw', - 'express.type': 'middleware', - 'http.route': '/a/b/c', - 'sentry.op': 'middleware', - 'sentry.origin': 'auto.http.express', - }, - description: 'mw', - }, - ]); - expect(span?.ended).toBe(false); - res.emit('finish'); - expect(span?.ended).toBe(true); - checkSpans([]); - }); - - it('names router spans after their route when span streaming is enabled', () => { - spanStreamingEnabled = true; - const options: ExpressPatchLayerOptions = {}; - const req = Object.assign(new EventEmitter(), { - originalUrl: '/a/b/c', - }) as unknown as ExpressRequest; - - const layer = { - name: 'router', - handle: vi.fn(), - } as unknown as ExpressLayer; - - const res = Object.assign(new EventEmitter(), {}) as unknown as ExpressResponse; - - storeLayer(req, '/a'); - storeLayer(req, '/b'); - - patchLayer(() => options, layer, '/c'); - layer.handle(req, res); - - checkSpans([ - { - status: { code: 0, message: 'OK' }, - data: { - 'express.name': '/c', - 'express.type': 'router', - 'http.route': '/a/b/c', - 'sentry.op': 'router', - 'sentry.origin': 'auto.http.express', - }, - description: '/a/b/c', - }, - ]); - }); - - it('falls back to a static router span name when the route is unknown', () => { - spanStreamingEnabled = true; - const options: ExpressPatchLayerOptions = {}; - const req = Object.assign(new EventEmitter(), { - originalUrl: '/abcdef', - }) as unknown as ExpressRequest; - - const layer = { - name: 'router', - handle: vi.fn(), - } as unknown as ExpressLayer; - - const res = Object.assign(new EventEmitter(), {}) as unknown as ExpressResponse; - - storeLayer(req, '/a'); - storeLayer(req, '/b'); - - patchLayer(() => options, layer, '/c'); - layer.handle(req, res); - - checkSpans([ - { - status: { code: 0, message: 'OK' }, - data: { - 'express.name': '/c', - 'express.type': 'router', - 'sentry.op': 'router', - 'sentry.origin': 'auto.http.express', - }, - description: 'Router', - }, - ]); - }); - - it('names request handler spans after their route when span streaming is enabled', () => { - spanStreamingEnabled = true; - const options: ExpressPatchLayerOptions = {}; - const req = Object.assign(new EventEmitter(), { - originalUrl: '/a/b/c', - }) as unknown as ExpressRequest; - - const layer = { - name: 'handle', - handle: vi.fn(), - } as unknown as ExpressLayer; - - const res = Object.assign(new EventEmitter(), {}) as unknown as ExpressResponse; - - storeLayer(req, '/a'); - storeLayer(req, '/b'); - - patchLayer(() => options, layer, '/c'); - layer.handle(req, res); - - checkSpans([ - { - status: { code: 0, message: 'OK' }, - data: { - 'express.name': '/a/b/c', - 'express.type': 'request_handler', - 'http.route': '/a/b/c', - 'sentry.op': 'handler', - 'sentry.origin': 'auto.http.express', - }, - description: '/a/b/c', - }, - ]); - res.emit('finish'); - checkSpans([]); - }); - - it('falls back to a static request handler span name when the route is unknown', () => { - spanStreamingEnabled = true; - const options: ExpressPatchLayerOptions = {}; - const req = Object.assign(new EventEmitter(), { - originalUrl: '/abcdef', - }) as unknown as ExpressRequest; - - const layer = { - name: 'handle', - handle: vi.fn(), - } as unknown as ExpressLayer; - - const res = Object.assign(new EventEmitter(), {}) as unknown as ExpressResponse; - - storeLayer(req, '/a'); - storeLayer(req, '/b'); - - patchLayer(() => options, layer, '/c'); - layer.handle(req, res); - - checkSpans([ - { - status: { code: 0, message: 'OK' }, - data: { - 'express.name': '/a/b/c', - 'express.type': 'request_handler', - 'sentry.op': 'handler', - 'sentry.origin': 'auto.http.express', - }, - description: 'Request handler', - }, - ]); - res.emit('finish'); - checkSpans([]); - }); - - it('handles case when route does not match url', () => { - const onRouteResolved = vi.fn(); - const options: ExpressPatchLayerOptions = { onRouteResolved }; - const req = Object.assign(new EventEmitter(), { - originalUrl: '/abcdef', - }) as unknown as ExpressRequest; - - const layerHandleOriginal = vi.fn(); - const layer = { - name: 'router', - handle: layerHandleOriginal, - } as unknown as ExpressLayer; - - const res = Object.assign(new EventEmitter(), {}) as unknown as ExpressResponse; - - storeLayer(req, '/a'); - storeLayer(req, '/b'); - - patchLayer(() => options, layer, '/c'); - layer.handle(req, res); - expect(onRouteResolved).toHaveBeenCalledExactlyOnceWith(undefined); - const span = mockSpans[0]; - checkSpans([ - { - status: { code: 0, message: 'OK' }, - data: { - 'express.name': '/c', - 'express.type': 'router', - 'sentry.op': 'router', - 'sentry.origin': 'auto.http.express', - }, - description: '/c', - }, - ]); - expect(span?.ended).toBe(true); - checkSpans([]); - }); - - it('wraps the callback', () => { - const options: ExpressPatchLayerOptions = {}; - - const layerHandleOriginal = vi.fn((...args) => { - expect(getStoredLayers(req)).toStrictEqual(['/a', '/b', '/c']); - (args[3] as Function)(); - // removes the added layer when the cb indicates it's done - expect(getStoredLayers(req)).toStrictEqual(['/a', '/b']); - }); - - const layer = { - name: 'mw', - handle: layerHandleOriginal, - } as unknown as ExpressLayer; - - const res = Object.assign(new EventEmitter(), {}) as unknown as ExpressResponse; - const req = Object.assign(new EventEmitter(), { - originalUrl: '/a/b/c', - res, - route: {}, - }) as unknown as ExpressRequest; - - storeLayer(req, '/a'); - storeLayer(req, '/b'); - patchLayer(() => options, layer, '/c'); - - expect(getStoredLayers(req)).toStrictEqual(['/a', '/b']); - const callback = vi.fn(() => { - expect(getStoredLayers(req)).toStrictEqual(['/a', '/b']); - }); - layer.handle(req, res, 'random', callback, 'whatever'); - expect(getStoredLayers(req)).toStrictEqual(['/a', '/b']); - - const span = mockSpans[0]; - checkSpans([ - { - status: { code: 0, message: 'OK' }, - data: { - 'express.name': 'mw', - 'express.type': 'middleware', - 'sentry.op': 'middleware', - 'sentry.origin': 'auto.http.express', - }, - description: 'mw', - }, - ]); - expect(span?.ended).toBe(true); - checkSpans([]); - }); - - it('handles callback being called with an error', () => { - const options: ExpressPatchLayerOptions = {}; - - const layerHandleOriginal = vi.fn((...args) => { - expect(getStoredLayers(req)).toStrictEqual(['/a', '/b', '/c']); - (args[3] as Function)(new Error('oopsie')); - // do not remove extra layer if this is where it failed though! - expect(getStoredLayers(req)).toStrictEqual(['/a', '/b', '/c']); - }); - - const layer = { - name: 'mw', - handle: layerHandleOriginal, - } as unknown as ExpressLayer; - - const res = Object.assign(new EventEmitter(), {}) as unknown as ExpressResponse; - const req = Object.assign(new EventEmitter(), { - originalUrl: '/a/b/c', - res, - route: {}, - }) as unknown as ExpressRequest; - - storeLayer(req, '/a'); - storeLayer(req, '/b'); - patchLayer(() => options, layer, '/c'); - - expect(getStoredLayers(req)).toStrictEqual(['/a', '/b']); - const callback = vi.fn(() => { - expect(getStoredLayers(req)).toStrictEqual(['/a', '/b', '/c']); - }); - layer.handle(req, res, 'random', callback, 'whatever'); - expect(getStoredLayers(req)).toStrictEqual(['/a', '/b', '/c']); - - const span = mockSpans[0]; - checkSpans([ - { - status: { code: 2, message: 'oopsie' }, - data: { - 'express.name': 'mw', - 'express.type': 'middleware', - 'sentry.op': 'middleware', - 'sentry.origin': 'auto.http.express', - }, - description: 'mw', - }, - ]); - expect(span?.ended).toBe(true); - checkSpans([]); - }); - - it('handles throws in layer.handle', () => { - const onRouteResolved = vi.fn(); - const options: ExpressPatchLayerOptions = { onRouteResolved }; - const req = Object.assign(new EventEmitter(), { - originalUrl: '/a/b/c/d', - }) as unknown as ExpressRequest; - - const layerHandleOriginal = vi.fn(() => { - throw new Error('yur head asplode'); - }); - const layer = { - name: 'mw', - handle: layerHandleOriginal, - } as unknown as ExpressLayer; - - const res = Object.assign(new EventEmitter(), {}) as unknown as ExpressResponse; - - storeLayer(req, '/a'); - storeLayer(req, '/b'); - - patchLayer(() => options, layer, '/c'); - expect(() => { - layer.handle(req, res); - }).toThrowError('yur head asplode'); - expect(onRouteResolved).toHaveBeenCalledExactlyOnceWith('/a/b/c'); - const span = mockSpans[0]; - checkSpans([ - { - status: { code: 2, message: 'yur head asplode' }, - data: { - 'express.name': 'mw', - 'express.type': 'middleware', - 'http.route': '/a/b/c', - 'sentry.op': 'middleware', - 'sentry.origin': 'auto.http.express', - }, - description: 'mw', - }, - ]); - expect(span?.ended).toBe(false); - res.emit('finish'); - expect(span?.ended).toBe(true); - checkSpans([]); - }); -}); diff --git a/packages/core/test/lib/integrations/express/request-layer-store.test.ts b/packages/core/test/lib/integrations/express/request-layer-store.test.ts deleted file mode 100644 index c2f58ac0e6f4..000000000000 --- a/packages/core/test/lib/integrations/express/request-layer-store.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import type { ExpressRequest } from '../../../../src/integrations/express/types'; -import { getStoredLayers, storeLayer } from '../../../../src/integrations/express/request-layer-store'; - -describe('storeLayer', () => { - it('handles case when nothing stored yet', () => { - const req = {} as unknown as ExpressRequest; - const empty = getStoredLayers(req); - expect(empty).toStrictEqual([]); - expect(getStoredLayers(req)).toStrictEqual([]); - }); - it('stores layer for a request', () => { - const req = {} as unknown as ExpressRequest; - storeLayer(req, 'a'); - storeLayer(req, 'b'); - expect(getStoredLayers(req)).toStrictEqual(['a', 'b']); - }); -}); diff --git a/packages/core/test/lib/integrations/express/set-sdk-processing-metadata.test.ts b/packages/core/test/lib/integrations/express/set-sdk-processing-metadata.test.ts deleted file mode 100644 index 21a6810b4279..000000000000 --- a/packages/core/test/lib/integrations/express/set-sdk-processing-metadata.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { vi, beforeEach, describe, it, expect } from 'vitest'; -import { setSDKProcessingMetadata } from '../../../../src/integrations/express/set-sdk-processing-metadata'; - -const sdkProcessingMetadatas: unknown[] = []; -beforeEach(() => (sdkProcessingMetadatas.length = 0)); -const isolationScope = { - _scopeData: {} as { sdkProcessingMetadata?: unknown }, - getScopeData() { - return this._scopeData; - }, - setSDKProcessingMetadata(data: unknown) { - this._scopeData.sdkProcessingMetadata = data; - sdkProcessingMetadatas.push(data); - }, -}; -vi.mock('../../../../src/currentScopes', () => ({ - getIsolationScope() { - return isolationScope; - }, -})); - -describe('setSDKProcessingMetadata', () => { - it('sets the normalized request data', () => { - const request = { - originalUrl: '/a/b/c', - route: '/a/:boo/:car', - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }; - setSDKProcessingMetadata(request); - // call it again to cover no-op branch - setSDKProcessingMetadata(request); - expect(JSON.stringify(sdkProcessingMetadatas)).toBe( - JSON.stringify([ - { - normalizedRequest: { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }, - }, - ]), - ); - }); -}); diff --git a/packages/core/test/lib/integrations/express/types.test.ts b/packages/core/test/lib/integrations/express/types.test.ts deleted file mode 100644 index 12fe68864b3d..000000000000 --- a/packages/core/test/lib/integrations/express/types.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as types from '../../../../src/integrations/express/types'; -import { describe, it, expect } from 'vitest'; - -// this is mostly just a types-bag, but it does have some constant keys -describe('types', () => { - it('exports several constants', () => { - // spread so it's a normal object - const { ...vals } = types; - expect(vals).toStrictEqual({ - ATTR_EXPRESS_NAME: 'express.name', - ATTR_HTTP_ROUTE: 'http.route', - ATTR_EXPRESS_TYPE: 'express.type', - ExpressLayerType_ROUTER: 'router', - ExpressLayerType_MIDDLEWARE: 'middleware', - ExpressLayerType_REQUEST_HANDLER: 'request_handler', - }); - }); -}); diff --git a/packages/core/test/lib/integrations/express/utils.test.ts b/packages/core/test/lib/integrations/express/utils.test.ts deleted file mode 100644 index a7ec32d96e8d..000000000000 --- a/packages/core/test/lib/integrations/express/utils.test.ts +++ /dev/null @@ -1,431 +0,0 @@ -import { storeLayer } from '../../../../src/integrations/express/request-layer-store'; -import { - ATTR_EXPRESS_NAME, - ATTR_EXPRESS_TYPE, - type MiddlewareError, - type ExpressIntegrationOptions, - type ExpressLayer, - type ExpressRequest, -} from '../../../../src/integrations/express/types'; -import { - asErrorAndMessage, - defaultShouldHandleError, - getActualMatchedRoute, - getConstructedRoute, - getLayerMetadata, - getLayerPath, - getRouterPath, - isExpressWithoutRouterPrototype, - isExpressWithRouterPrototype, - isLayerIgnored, - isRoutePattern, -} from '../../../../src/integrations/express/utils'; - -import { describe, it, expect } from 'vitest'; - -describe('asErrorAndMessage', () => { - it('returns an Error with its message', () => { - const er = new Error('message'); - expect(asErrorAndMessage(er)).toStrictEqual([er, 'message']); - }); - it('returns an non-Error cast to string', () => { - const er = { - toString() { - return 'message'; - }, - }; - expect(asErrorAndMessage(er)).toStrictEqual(['message', 'message']); - }); -}); - -describe('isRoutePattern', () => { - it('searches for : and *', () => { - expect(isRoutePattern('a:b')).toBe(true); - expect(isRoutePattern('abc*')).toBe(true); - expect(isRoutePattern('abc')).toBe(false); - }); -}); - -describe('getRouterPath', () => { - it('reconstructs returns path if layer is empty', () => { - expect(getRouterPath('/a', {} as unknown as ExpressLayer)).toBe('/a'); - expect( - getRouterPath('/a', { - handle: {}, - } as unknown as ExpressLayer), - ).toBe('/a'); - expect( - getRouterPath('/a', { - handle: { stack: [] }, - } as unknown as ExpressLayer), - ).toBe('/a'); - expect( - getRouterPath('/a', { - handle: { - stack: [ - { - handle: { - stack: [ - { - handle: { - stack: [], - }, - }, - ], - }, - }, - ], - }, - } as unknown as ExpressLayer), - ).toBe('/a'); - }); - - it('uses the stackLayer route path if present', () => { - expect( - getRouterPath('/a', { - handle: { - stack: [{ route: { path: '/b' } }], - }, - } as unknown as ExpressLayer), - ).toBe('/a/b'); - }); - - it('recurses to search layer stack', () => { - expect( - getRouterPath('/a', { - handle: { - stack: [ - { - handle: { - stack: [ - { - handle: { - stack: [{ route: { path: '/b' } }], - }, - }, - ], - }, - }, - ], - }, - } as unknown as ExpressLayer), - ).toBe('/a/b'); - }); -}); - -describe('getLayerMetadata', () => { - it('returns the metadata from router layer', () => { - expect( - getLayerMetadata('/a', { - name: 'router', - route: { path: '/b' }, - } as unknown as ExpressLayer), - ).toStrictEqual({ - attributes: { - [ATTR_EXPRESS_NAME]: '/a', - [ATTR_EXPRESS_TYPE]: 'router', - }, - name: 'router - /a', - }); - expect( - getLayerMetadata( - '/a', - { - name: 'router', - route: {}, - } as unknown as ExpressLayer, - '/c', - ), - ).toStrictEqual({ - attributes: { - [ATTR_EXPRESS_NAME]: '/c', - [ATTR_EXPRESS_TYPE]: 'router', - }, - name: 'router - /c', - }); - expect( - getLayerMetadata('/a', { - name: 'router', - route: {}, - } as unknown as ExpressLayer), - ).toStrictEqual({ - attributes: { - [ATTR_EXPRESS_NAME]: '/a', - [ATTR_EXPRESS_TYPE]: 'router', - }, - name: 'router - /a', - }); - expect( - getLayerMetadata('', { - name: 'router', - route: {}, - } as unknown as ExpressLayer), - ).toStrictEqual({ - attributes: { - [ATTR_EXPRESS_NAME]: '/', - [ATTR_EXPRESS_TYPE]: 'router', - }, - name: 'router - /', - }); - expect( - getLayerMetadata('', { - name: 'router', - handle: { - stack: [{ route: { path: '/b' } }], - }, - } as unknown as ExpressLayer), - ).toStrictEqual({ - attributes: { - [ATTR_EXPRESS_NAME]: '/b', - [ATTR_EXPRESS_TYPE]: 'router', - }, - name: 'router - /b', - }); - expect( - getLayerMetadata('', { - name: 'bound dispatch', - handle: { - stack: [{ route: { path: '/b' } }], - }, - } as unknown as ExpressLayer), - ).toStrictEqual({ - attributes: { - [ATTR_EXPRESS_NAME]: 'request handler', - [ATTR_EXPRESS_TYPE]: 'request_handler', - }, - name: 'request handler', - }); - expect( - getLayerMetadata('/r', { - name: 'handle', - path: '/l', - handle: { - stack: [{ route: { path: '/b' } }], - }, - } as unknown as ExpressLayer), - ).toStrictEqual({ - attributes: { - [ATTR_EXPRESS_NAME]: '/r', - [ATTR_EXPRESS_TYPE]: 'request_handler', - }, - name: 'request handler - /r', - }); - expect( - getLayerMetadata( - '', - { - name: 'handle', - path: '/l', - handle: { - stack: [{ route: { path: '/b' } }], - }, - } as unknown as ExpressLayer, - '/x', - ), - ).toStrictEqual({ - attributes: { - [ATTR_EXPRESS_NAME]: '/x', - [ATTR_EXPRESS_TYPE]: 'request_handler', - }, - name: 'request handler - /x', - }); - expect( - getLayerMetadata( - '', - { - name: 'some_other_thing', - path: '/l', - handle: { - stack: [{ route: { path: '/b' } }], - }, - } as unknown as ExpressLayer, - '/x', - ), - ).toStrictEqual({ - attributes: { - [ATTR_EXPRESS_NAME]: 'some_other_thing', - [ATTR_EXPRESS_TYPE]: 'middleware', - }, - name: 'middleware - some_other_thing', - }); - }); -}); - -describe('isLayerIgnored', () => { - it('ignores layers that include the ignored type', () => { - expect( - isLayerIgnored('x', 'router', { - ignoreLayersType: ['router'], - } as unknown as ExpressIntegrationOptions), - ).toBe(true); - - expect( - isLayerIgnored('x', 'router', { - ignoreLayers: [/^x$/], - } as unknown as ExpressIntegrationOptions), - ).toBe(true); - - expect( - isLayerIgnored('x', 'router', { - ignoreLayers: ['x'], - } as unknown as ExpressIntegrationOptions), - ).toBe(true); - expect( - isLayerIgnored('x', 'router', { - ignoreLayers: [() => true], - } as unknown as ExpressIntegrationOptions), - ).toBe(true); - - expect(isLayerIgnored('x', 'router', {} as unknown as ExpressIntegrationOptions)).toBe(false); - expect( - isLayerIgnored('x', 'router', { - ignoreLayersType: ['middleware'], - } as unknown as ExpressIntegrationOptions), - ).toBe(false); - expect( - isLayerIgnored('x', 'router', { - ignoreLayers: [() => false], - } as unknown as ExpressIntegrationOptions), - ).toBe(false); - expect( - isLayerIgnored('x', 'router', { - ignoreLayers: [ - () => { - throw new Error('x'); - }, - ], - } as unknown as ExpressIntegrationOptions), - ).toBe(false); - }); -}); - -describe('getActualMatchedRoute', () => { - it('handles empty layersStore', () => { - const req = {} as unknown as ExpressRequest; - expect(getActualMatchedRoute(req, getConstructedRoute(req))).toBe(undefined); - }); - - it('handles case when all stored layers are /', () => { - const req = { originalUrl: '/' } as unknown as ExpressRequest; - storeLayer(req, '/'); - expect(getActualMatchedRoute(req, getConstructedRoute(req))).toBe('/'); - req.originalUrl = '/other-thing'; - expect(getActualMatchedRoute(req, getConstructedRoute(req))).toBe(undefined); - }); - - it('returns constructed route if *', () => { - const req = { originalUrl: '/xyz' } as unknown as ExpressRequest; - storeLayer(req, '*'); - expect(getActualMatchedRoute(req, getConstructedRoute(req))).toBe('*'); - }); - - it('returns constructed route when it looks regexp-ish', () => { - const req = { originalUrl: '/xyz' } as unknown as ExpressRequest; - storeLayer(req, '/\\,[*]/'); - expect(getActualMatchedRoute(req, getConstructedRoute(req))).toBe('/\\,[*]/'); - }); - - it('ensures constructed route starts with /', () => { - const req = { originalUrl: '/a/b' } as unknown as ExpressRequest; - storeLayer(req, 'a'); - storeLayer(req, '/b'); - expect(getActualMatchedRoute(req, getConstructedRoute(req))).toBe('/a/b'); - }); - - it('allows routes that contain *', () => { - const req = { originalUrl: '/a/b' } as unknown as ExpressRequest; - storeLayer(req, 'a'); - storeLayer(req, '/:boo'); - expect(getActualMatchedRoute(req, getConstructedRoute(req))).toBe('/a/:boo'); - }); - - it('returns undefined if invalid', () => { - const req = { originalUrl: '/a/b' } as unknown as ExpressRequest; - storeLayer(req, '/a'); - storeLayer(req, '/c'); - expect(getActualMatchedRoute(req, getConstructedRoute(req))).toBe(undefined); - }); -}); - -describe('getConstructedRoute', () => { - it('returns * when the only meaningful path', () => { - const req = {} as unknown as ExpressRequest; - storeLayer(req, '*'); - // not-meaningful paths - storeLayer(req, '/'); - storeLayer(req, '/*'); - expect(getConstructedRoute(req)).toBe('*'); - }); - - it('joins meaningful paths together', () => { - const req = {} as unknown as ExpressRequest; - storeLayer(req, '/a'); - storeLayer(req, '/b/'); - storeLayer(req, '/*'); - storeLayer(req, '/c'); - expect(getConstructedRoute(req)).toBe('/a/b/c'); - }); -}); - -describe('isExpressWith(out)RouterPrototype', () => { - it('detects what kind of express this is', () => { - expect(isExpressWithoutRouterPrototype({})).toBe(false); - expect( - isExpressWithoutRouterPrototype( - Object.assign(function express() {}, { - Router: Object.assign(function Router() {}, { - route() {}, - }), - }), - ), - ).toBe(true); - expect( - isExpressWithoutRouterPrototype( - Object.assign(function express() {}, { - Router: class Router { - route() {} - }, - }), - ), - ).toBe(false); - expect(isExpressWithRouterPrototype({})).toBe(false); - expect( - isExpressWithRouterPrototype({ - Router: Object.assign(function Router() {}, { - route() {}, - }), - }), - ).toBe(false); - expect( - isExpressWithRouterPrototype({ - Router: class Router { - route() {} - }, - }), - ).toBe(true); - }); -}); - -describe('getLayerPath', () => { - it('extracts the layer path segment from first arg', () => { - expect(getLayerPath(['/x'])).toBe('/x'); - expect(getLayerPath([['/x', '/y']])).toBe('/x,/y'); - expect(getLayerPath([['/x', null, 1, /z/i, '/y']])).toBe('/x,,1,/z/i,/y'); - }); -}); - -describe('defaultShouldHandleError', () => { - it('returns true if the response status code is 500', () => { - // just a wrapper to not have to type this out each time. - const _ = (o: unknown): MiddlewareError => o as MiddlewareError; - expect(defaultShouldHandleError(_({ status: 500 }))).toBe(true); - expect(defaultShouldHandleError(_({ statusCode: 500 }))).toBe(true); - expect(defaultShouldHandleError(_({ status_code: 500 }))).toBe(true); - expect(defaultShouldHandleError(_({ output: { statusCode: 500 } }))).toBe(true); - expect(defaultShouldHandleError(_({}))).toBe(true); - expect(defaultShouldHandleError(_({ status: 200 }))).toBe(false); - expect(defaultShouldHandleError(_({ statusCode: 200 }))).toBe(false); - expect(defaultShouldHandleError(_({ status_code: 200 }))).toBe(false); - expect(defaultShouldHandleError(_({ output: { statusCode: 200 } }))).toBe(false); - }); -});