UNPKG

@sentry/node

Version:

Sentry Node SDK using OpenTelemetry for performance instrumentation

275 lines (271 loc) • 10.2 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); const url = require('url'); const core = require('@sentry/core'); const serverUtils = require('@sentry/server-utils'); const outgoingFetchRequest = require('../../utils/outgoingFetchRequest.js'); const attributes = require('@sentry/conventions/attributes'); const op = require('@sentry/conventions/op'); const debugBuild = require('../../debug-build.js'); const ATTR_HTTP_REQUEST_METHOD_ORIGINAL = "http.request.method_original"; let _isInstrumented = false; const spanFromReq = /* @__PURE__ */ new WeakMap(); const ignoreRequestMap = /* @__PURE__ */ new WeakMap(); const propagationDecisionMap = new core.LRUMap(100); function instrumentUndici(config = {}) { if (_isInstrumented) { return; } _isInstrumented = true; subscribeToChannel("undici:request:create", (message) => onRequestCreated(config, message)); subscribeToChannel( "undici:client:sendHeaders", (message) => onRequestHeaders(config, message) ); subscribeToChannel("undici:request:headers", (message) => onResponseHeaders(config, message)); subscribeToChannel("undici:request:trailers", (message) => onDone(message)); subscribeToChannel("undici:request:error", (message) => onError(message)); } function subscribeToChannel(diagnosticChannel, onMessage) { serverUtils.subscribeDiagnosticsChannel(diagnosticChannel, onMessage); } function parseRequestHeaders(request) { const result = /* @__PURE__ */ new Map(); if (Array.isArray(request.headers)) { for (let i = 0; i < request.headers.length; i += 2) { const key = request.headers[i]; const value = request.headers[i + 1]; if (typeof key === "string" && value !== void 0) { result.set(key.toLowerCase(), value); } } } else if (typeof request.headers === "string") { const headers = request.headers.split("\r\n"); for (const line of headers) { if (!line) { continue; } const colonIndex = line.indexOf(":"); if (colonIndex === -1) { continue; } const key = line.substring(0, colonIndex).toLowerCase(); const value = line.substring(colonIndex + 1).trim(); const allValues = result.get(key); if (allValues && Array.isArray(allValues)) { allValues.push(value); } else if (allValues) { result.set(key, [allValues, value]); } else { result.set(key, value); } } } return result; } function onRequestCreated(config, { request }) { const url$1 = getAbsoluteUrl(request.origin, request.path); const ignoredByCallback = core.safeCallback( debugBuild.DEBUG_BUILD ? "The `ignoreOutgoingRequests` callback threw an error, not ignoring the request:" : "", () => !!config.ignoreOutgoingRequests?.(url$1), () => false ); const ignoreForBreadcrumbs = core.isTracingSuppressed() || !!ignoredByCallback; ignoreRequestMap.set(request, ignoreForBreadcrumbs); if (!config.spans) { if (config.tracePropagation !== false && !ignoreForBreadcrumbs) { outgoingFetchRequest.addTracePropagationHeadersToFetchRequest(request, propagationDecisionMap); } return; } if (request.method === "CONNECT" || ignoredByCallback) { return; } let requestUrl; try { requestUrl = new url.URL(request.path, request.origin); } catch (err) { debugBuild.DEBUG_BUILD && core.debug.warn("could not determine url.full:", err); return; } const urlScheme = requestUrl.protocol.replace(":", ""); const requestMethod = getRequestMethod(request.method); const attributes$1 = { [attributes.SENTRY_KIND]: "client", [attributes.SENTRY_OP]: op.HTTP_CLIENT, [attributes.HTTP_REQUEST_METHOD]: requestMethod, [ATTR_HTTP_REQUEST_METHOD_ORIGINAL]: request.method, [attributes.URL_FULL]: core.filterCollectedUrl(requestUrl.toString()), [attributes.URL_DOMAIN]: requestUrl.hostname || void 0, [attributes.URL_PATH]: requestUrl.pathname, [attributes.URL_QUERY]: core.filterCollectedUrlQuery(core.getUrlQuery(requestUrl.search)), [attributes.URL_FRAGMENT]: core.getUrlFragment(requestUrl.hash), [attributes.URL_SCHEME]: urlScheme, [attributes.SENTRY_ORIGIN]: "auto.http.node_fetch" }; if (url$1.startsWith("data:")) { const sanitizedUrl = core.stripDataUrlContent(url$1); attributes$1[attributes.URL_FULL] = sanitizedUrl; attributes$1[core.SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME] = `${request.method || "GET"} ${sanitizedUrl}`; } const schemePorts = { https: "443", http: "80" }; const serverAddress = requestUrl.hostname; const serverPort = requestUrl.port || schemePorts[urlScheme]; attributes$1[attributes.SERVER_ADDRESS] = serverAddress; if (serverPort && !isNaN(Number(serverPort))) { attributes$1[attributes.SERVER_PORT] = Number(serverPort); } const headersMap = parseRequestHeaders(request); const userAgentValues = headersMap.get("user-agent"); if (userAgentValues) { const userAgent = Array.isArray(userAgentValues) ? userAgentValues[userAgentValues.length - 1] : userAgentValues; attributes$1[attributes.USER_AGENT_ORIGINAL] = userAgent; } const isDataUrl = url$1.startsWith("data:"); const client = core.getClient(); let spanName; if (requestMethod === "_OTHER") { spanName = core.HTTP_SPAN_NAME_FALLBACK; } else if (!!client && core.hasSpanStreamingEnabled(client)) { spanName = requestUrl.hostname ? `${requestMethod} ${requestUrl.hostname}` : requestMethod; } else if (isDataUrl) { spanName = `${request.method || "GET"} ${core.stripDataUrlContent(url$1)}`; } else { spanName = `${requestMethod} ${core.getSanitizedUrlString(core.parseUrl(requestUrl.toString()))}`; } const span = core.startInactiveSpan({ name: spanName, attributes: attributes$1, onlyIfParent: !client || !core.hasSpanStreamingEnabled(client) }); core.safeCallback( debugBuild.DEBUG_BUILD ? "The `requestHook` callback threw an error:" : "", () => config.requestHook?.(span, request), () => void 0 ); outgoingFetchRequest.addTracePropagationHeadersToFetchRequest(request, propagationDecisionMap, span); spanFromReq.set(request, span); } function onRequestHeaders(config, { request, socket }) { const span = spanFromReq.get(request); if (!span) { return; } const { remoteAddress, remotePort } = socket; const spanAttributes = { [attributes.NETWORK_PEER_ADDRESS]: remoteAddress, [attributes.NETWORK_PEER_PORT]: remotePort }; const client = core.getClient(); if (config.headersToSpanAttributes?.requestHeaders && client) { const headersToAttribs = new Set(config.headersToSpanAttributes.requestHeaders.map((n) => n.toLowerCase())); const headersMap = parseRequestHeaders(request); const allowlisted = {}; for (const [name, value] of headersMap.entries()) { if (headersToAttribs.has(name)) { allowlisted[name] = value; } } Object.assign(spanAttributes, core.httpHeadersToSpanAttributes(allowlisted, client.getDataCollectionOptions())); } span.setAttributes(spanAttributes); } function onResponseHeaders(config, { request, response }) { const breadcrumbsEnabled = config.breadcrumbs !== false; if (breadcrumbsEnabled && !ignoreRequestMap.get(request)) { outgoingFetchRequest.addFetchRequestBreadcrumb(request, response); } const span = spanFromReq.get(request); if (!span) { return; } const spanAttributes = { [attributes.HTTP_RESPONSE_STATUS_CODE]: response.statusCode }; core.safeCallback( debugBuild.DEBUG_BUILD ? "The `responseHook` callback threw an error:" : "", () => config.responseHook?.(span, { request, response }), () => void 0 ); const client = core.getClient(); if (config.headersToSpanAttributes?.responseHeaders && client) { const headersToAttribs = /* @__PURE__ */ new Set(); config.headersToSpanAttributes?.responseHeaders.forEach((name) => headersToAttribs.add(name.toLowerCase())); const allowlisted = {}; for (let idx = 0; idx < response.headers.length; idx = idx + 2) { const nameBuf = response.headers[idx]; const valueBuf = response.headers[idx + 1]; if (nameBuf === void 0 || valueBuf === void 0) { continue; } const name = nameBuf.toString().toLowerCase(); if (headersToAttribs.has(name)) { (allowlisted[name] ?? (allowlisted[name] = [])).push(valueBuf.toString()); } } Object.assign( spanAttributes, core.httpHeadersToSpanAttributes(allowlisted, client.getDataCollectionOptions(), "response") ); } span.setAttributes(spanAttributes); if (response.statusCode >= 400) { span.setStatus(core.getSpanStatusFromHttpCode(response.statusCode)); } } function onDone({ request }) { const span = spanFromReq.get(request); if (!span) { return; } span.end(); spanFromReq.delete(request); } function onError({ request, error }) { const span = spanFromReq.get(request); if (!span) { return; } const isAbort = error.name === "AbortError" || typeof DOMException !== "undefined" && error instanceof DOMException && error.code === DOMException.ABORT_ERR; if (isAbort) { span.end(); spanFromReq.delete(request); return; } span.setStatus({ code: core.SPAN_STATUS_ERROR, message: error.message }); span.end(); spanFromReq.delete(request); } function getRequestMethod(original) { const knownMethods = { CONNECT: true, OPTIONS: true, HEAD: true, GET: true, POST: true, PUT: true, PATCH: true, DELETE: true, TRACE: true, // QUERY from https://datatracker.ietf.org/doc/rfc10008/ QUERY: true }; if (original.toUpperCase() in knownMethods) { return original.toUpperCase(); } return "_OTHER"; } function getAbsoluteUrl(origin, path = "/") { const url = `${origin}`; if (url.endsWith("/") && path.startsWith("/")) { return `${url}${path.slice(1)}`; } if (!url.endsWith("/") && !path.startsWith("/")) { return `${url}/${path}`; } return `${url}${path}`; } exports.instrumentUndici = instrumentUndici; //# sourceMappingURL=undici-instrumentation.js.map