@sentry/node
Version:
Sentry Node SDK using OpenTelemetry for performance instrumentation
273 lines (270 loc) • 10.3 kB
JavaScript
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