UNPKG

@sentry/node

Version:

Sentry Node SDK using OpenTelemetry for performance instrumentation

273 lines (270 loc) • 10.3 kB
import { URL } from 'url'; import { LRUMap, safeCallback, isTracingSuppressed, debug, getUrlFragment, filterCollectedUrlQuery, getUrlQuery, filterCollectedUrl, stripDataUrlContent, getClient, hasSpanStreamingEnabled, getSanitizedUrlString, parseUrl, startInactiveSpan, httpHeadersToSpanAttributes, getSpanStatusFromHttpCode, SPAN_STATUS_ERROR, SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME, HTTP_SPAN_NAME_FALLBACK } from '@sentry/core'; import { subscribeDiagnosticsChannel } from '@sentry/server-utils'; import { addTracePropagationHeadersToFetchRequest, addFetchRequestBreadcrumb } from '../../utils/outgoingFetchRequest.js'; import { URL_FRAGMENT, URL_QUERY, URL_PATH, URL_DOMAIN, URL_FULL, HTTP_RESPONSE_STATUS_CODE, SENTRY_ORIGIN, URL_SCHEME, HTTP_REQUEST_METHOD, SENTRY_OP, SENTRY_KIND, SERVER_ADDRESS, SERVER_PORT, USER_AGENT_ORIGINAL, NETWORK_PEER_PORT, NETWORK_PEER_ADDRESS } from '@sentry/conventions/attributes'; import { HTTP_CLIENT } from '@sentry/conventions/op'; import { DEBUG_BUILD } from '../../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 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) { 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 = getAbsoluteUrl(request.origin, request.path); const ignoredByCallback = safeCallback( DEBUG_BUILD ? "The `ignoreOutgoingRequests` callback threw an error, not ignoring the request:" : "", () => !!config.ignoreOutgoingRequests?.(url), () => false ); const ignoreForBreadcrumbs = isTracingSuppressed() || !!ignoredByCallback; ignoreRequestMap.set(request, ignoreForBreadcrumbs); if (!config.spans) { if (config.tracePropagation !== false && !ignoreForBreadcrumbs) { addTracePropagationHeadersToFetchRequest(request, propagationDecisionMap); } return; } if (request.method === "CONNECT" || ignoredByCallback) { return; } let requestUrl; try { requestUrl = new URL(request.path, request.origin); } catch (err) { DEBUG_BUILD && debug.warn("could not determine url.full:", err); return; } const urlScheme = requestUrl.protocol.replace(":", ""); const requestMethod = getRequestMethod(request.method); const attributes = { [SENTRY_KIND]: "client", [SENTRY_OP]: HTTP_CLIENT, [HTTP_REQUEST_METHOD]: requestMethod, [ATTR_HTTP_REQUEST_METHOD_ORIGINAL]: request.method, [URL_FULL]: filterCollectedUrl(requestUrl.toString()), [URL_DOMAIN]: requestUrl.hostname || void 0, [URL_PATH]: requestUrl.pathname, [URL_QUERY]: filterCollectedUrlQuery(getUrlQuery(requestUrl.search)), [URL_FRAGMENT]: getUrlFragment(requestUrl.hash), [URL_SCHEME]: urlScheme, [SENTRY_ORIGIN]: "auto.http.node_fetch" }; if (url.startsWith("data:")) { const sanitizedUrl = stripDataUrlContent(url); attributes[URL_FULL] = sanitizedUrl; attributes[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[SERVER_ADDRESS] = serverAddress; if (serverPort && !isNaN(Number(serverPort))) { 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[USER_AGENT_ORIGINAL] = userAgent; } const isDataUrl = url.startsWith("data:"); const client = getClient(); let spanName; if (requestMethod === "_OTHER") { spanName = HTTP_SPAN_NAME_FALLBACK; } else if (!!client && hasSpanStreamingEnabled(client)) { spanName = requestUrl.hostname ? `${requestMethod} ${requestUrl.hostname}` : requestMethod; } else if (isDataUrl) { spanName = `${request.method || "GET"} ${stripDataUrlContent(url)}`; } else { spanName = `${requestMethod} ${getSanitizedUrlString(parseUrl(requestUrl.toString()))}`; } const span = startInactiveSpan({ name: spanName, attributes, onlyIfParent: !client || !hasSpanStreamingEnabled(client) }); safeCallback( DEBUG_BUILD ? "The `requestHook` callback threw an error:" : "", () => config.requestHook?.(span, request), () => void 0 ); 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 = { [NETWORK_PEER_ADDRESS]: remoteAddress, [NETWORK_PEER_PORT]: remotePort }; const client = 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, httpHeadersToSpanAttributes(allowlisted, client.getDataCollectionOptions())); } span.setAttributes(spanAttributes); } function onResponseHeaders(config, { request, response }) { const breadcrumbsEnabled = config.breadcrumbs !== false; if (breadcrumbsEnabled && !ignoreRequestMap.get(request)) { addFetchRequestBreadcrumb(request, response); } const span = spanFromReq.get(request); if (!span) { return; } const spanAttributes = { [HTTP_RESPONSE_STATUS_CODE]: response.statusCode }; safeCallback( DEBUG_BUILD ? "The `responseHook` callback threw an error:" : "", () => config.responseHook?.(span, { request, response }), () => void 0 ); const client = 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, httpHeadersToSpanAttributes(allowlisted, client.getDataCollectionOptions(), "response") ); } span.setAttributes(spanAttributes); if (response.statusCode >= 400) { span.setStatus(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: 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}`; } export { instrumentUndici }; //# sourceMappingURL=undici-instrumentation.js.map