UNPKG

@sentry/core

Version:
256 lines (230 loc) 9.69 kB
import { DEBUG_BUILD } from '../../debug-build.js'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes.js'; import { debug } from '../../utils/debug-logger.js'; import { getActiveSpan } from '../../utils/spanUtils.js'; import { SPAN_STATUS_ERROR } from '../../tracing/spanstatus.js'; import { getOriginalFunction, markFunctionWrapped } from '../../utils/object.js'; import { getIsolationScope } from '../../currentScopes.js'; import { startSpanManual, withActiveSpan } from '../../tracing/trace.js'; import { storeLayer, getStoredLayers } from './request-layer-store.js'; import { ATTR_EXPRESS_NAME, ATTR_EXPRESS_TYPE, ATTR_HTTP_ROUTE, ExpressLayerType_ROUTER } from './types.js'; import { getConstructedRoute, getActualMatchedRoute, getLayerMetadata, isLayerIgnored, asErrorAndMessage } from './utils.js'; import { getDefaultIsolationScope } from '../../defaultScopes.js'; import { setSDKProcessingMetadata } from './set-sdk-processing-metadata.js'; /** * Platform-portable Express tracing integration. * * @module * * This Sentry integration is a derivative work based on the OpenTelemetry * Express instrumentation. * * <https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-express> * * 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. */ function patchLayer( getOptions, maybeLayer, layerPath, ) { 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( req, res, //oxlint-disable-next-line no-explicit-any ...otherArgs ) { 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 = Object.assign(metadata.attributes, { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.express', [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `${type}.express`, }); if (actualMatchedRoute) { attributes[ATTR_HTTP_ROUTE] = 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'); } return startSpanManual({ name, 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] ; if (typeof callback !== 'function') { continue; } //oxlint-disable-next-line no-explicit-any otherArgs[i] = function (...args) { // 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 ) { // 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, }); } export { patchLayer }; //# sourceMappingURL=patch-layer.js.map