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