@sentry/core
Version:
Base implementation for all Sentry JavaScript SDKs
286 lines (283 loc) • 12 kB
JavaScript
import { HTTP_ON_SERVER_REQUEST } from './constants.js';
import { DEBUG_BUILD } from '../../debug-build.js';
import { debug } from '../../utils/debug-logger.js';
import { getIsolationScope, getClient, withIsolationScope, getCurrentScope } from '../../currentScopes.js';
import { hasSpansEnabled } from '../../utils/hasSpansEnabled.js';
import { httpHeadersToSpanAttributes, headersToDict, httpRequestToRequestData } from '../../utils/request.js';
import { patchRequestToCaptureBody } from './patch-request-to-capture-body.js';
import { stripUrlQueryAndFragment, parseStringToURLObject } from '../../utils/url.js';
import { recordRequestSession } from './record-request-session.js';
import { generateSpanId, generateTraceId } from '../../utils/propagationContext.js';
import { startSpanManual, continueTrace } from '../../tracing/trace.js';
import { safeMathRandom } from '../../utils/randomSafeContext.js';
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_OP } from '../../semanticAttributes.js';
import { getSpanStatusFromHttpCode, SPAN_STATUS_ERROR } from '../../tracing/spanstatus.js';
const INTEGRATION_NAME = "Http.Server";
const SPANS_INTEGRATION_NAME = "Http.SentryServerSpans";
const lastSentryEmitMap = /* @__PURE__ */ new WeakMap();
const kRequestMark = /* @__PURE__ */ Symbol.for("sentry_http_server_instrumented");
function markRequest(request) {
return !request[kRequestMark] && (request[kRequestMark] = true);
}
function instrumentServer(options, server) {
const currentEmit = server.emit;
const instrumentedEmit = lastSentryEmitMap.get(server);
if (currentEmit === instrumentedEmit) {
return;
}
const newEmit = new Proxy(currentEmit, {
apply(target, thisArg, args) {
const [event, ...data] = args;
if (event !== "request") {
return target.apply(thisArg, args);
}
const client = getClient();
const [request, response] = data;
if (!client || !markRequest(request)) {
return target.apply(thisArg, args);
}
DEBUG_BUILD && debug.log(INTEGRATION_NAME, "Handling incoming request");
const isolationScope = getIsolationScope().clone();
isolationScope.setClient(client);
const ipAddress = request.socket?.remoteAddress;
const url = request.url || "/";
const normalizedRequest = httpRequestToRequestData(request);
const {
maxRequestBodySize = "medium",
ignoreRequestBody,
sessions = true,
sessionFlushingDelayMS = 6e4
} = options;
if (maxRequestBodySize !== "none" && !ignoreRequestBody?.(url, request)) {
patchRequestToCaptureBody(request, isolationScope, maxRequestBodySize, INTEGRATION_NAME);
}
isolationScope.setSDKProcessingMetadata({ normalizedRequest, ipAddress });
const httpMethod = (request.method || "GET").toUpperCase();
const httpTargetWithoutQueryFragment = stripUrlQueryAndFragment(url);
const bestEffortTransactionName = `${httpMethod} ${httpTargetWithoutQueryFragment}`;
isolationScope.setTransactionName(bestEffortTransactionName);
if (sessions) {
recordRequestSession(client, {
requestIsolationScope: isolationScope,
response,
sessionFlushingDelayMS: sessionFlushingDelayMS ?? 6e4
});
}
return withIsolationScope(isolationScope, () => {
const sentryTrace = normalizedRequest.headers?.["sentry-trace"];
const baggage = normalizedRequest.headers?.["baggage"];
const sentryTraceValue = Array.isArray(sentryTrace) ? sentryTrace[0] : sentryTrace;
return continueTrace(
{
sentryTrace: sentryTraceValue,
baggage: Array.isArray(baggage) ? baggage[0] : baggage
},
() => {
const propagationContext = getCurrentScope().getPropagationContext();
propagationContext.propagationSpanId = generateSpanId();
if (!sentryTraceValue) {
propagationContext.traceId = generateTraceId();
propagationContext.sampleRand = safeMathRandom();
}
response.once("close", () => {
isolationScope.setContext("response", {
status_code: response.statusCode
});
});
const wrap = options.wrapServerEmitRequest;
let emitResult = false;
if (wrap) {
wrap(request, response, normalizedRequest, () => {
emitResult = target.apply(thisArg, args);
});
} else {
emitResult = target.apply(thisArg, args);
}
return emitResult;
}
);
});
}
});
lastSentryEmitMap.set(server, newEmit);
server.emit = newEmit;
}
function getHttpServerSubscriptions(options) {
const userWrap = options.wrapServerEmitRequest;
const spanWrap = buildServerSpanWrap(options);
const effectiveOptions = {
...options,
wrapServerEmitRequest(request, response, normalizedRequest, next) {
const clientOptions = getClient()?.getOptions();
const createSpans = options.spans ?? (clientOptions ? hasSpansEnabled(clientOptions) : false);
if (createSpans) {
spanWrap(request, response, normalizedRequest, next);
} else if (userWrap) {
userWrap(request, response, normalizedRequest, next);
} else {
next();
}
}
};
const onHttpServerRequest = (data) => {
const { server } = data;
instrumentServer(effectiveOptions, server);
};
return { [HTTP_ON_SERVER_REQUEST]: onHttpServerRequest };
}
function buildServerSpanWrap(options) {
const {
wrapServerEmitRequest: userWrap,
ignoreIncomingRequests,
ignoreStaticAssets = true,
onSpanCreated,
errorMonitor = "error",
onSpanEnd
} = options;
return (request, response, normalizedRequest, next) => {
if (typeof __SENTRY_TRACING__ !== "undefined" && !__SENTRY_TRACING__) {
return next();
}
return userWrap ? userWrap(request, response, normalizedRequest, createSpan) : createSpan();
function createSpan() {
const isolationScope = getIsolationScope();
const client = isolationScope.getClient();
if (!client) {
return next();
}
if (shouldIgnoreSpansForIncomingRequest(request, {
ignoreStaticAssets,
ignoreIncomingRequests
})) {
DEBUG_BUILD && debug.log(SPANS_INTEGRATION_NAME, "Skipping span creation for incoming request", request.url);
return next();
}
const fullUrl = normalizedRequest.url || request.url || "/";
const urlObj = parseStringToURLObject(fullUrl);
const httpTargetWithoutQueryFragment = urlObj ? urlObj.pathname : stripUrlQueryAndFragment(fullUrl);
const method = (request.method || "GET").toUpperCase();
const name = `${method} ${httpTargetWithoutQueryFragment}`;
const headers = request.headers;
const userAgent = headers["user-agent"];
const ips = headers["x-forwarded-for"];
const httpVersion = request.httpVersion;
const host = headers.host;
const hostname = host?.replace(/^(.*)(:[0-9]{1,5})/, "$1") || "localhost";
const scheme = fullUrl.startsWith("https") ? "https" : "http";
const { socket } = request;
const { localAddress, localPort, remoteAddress, remotePort } = socket ?? {};
return startSpanManual(
{
name,
// SpanKind.SERVER = 1; pass this so the OTel sampler infers
// op='http.server' rather than 'http', which it does for
// SpanKind.INTERNAL = 0, the default
kind: 1,
attributes: {
// Sentry-specific attributes
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: "http.server",
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: "auto.http.server",
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: "url",
// Set http.route to the URL path as a best-effort route name.
// Framework integrations (Express, etc.) update this via onSpanEnd.
"http.route": httpTargetWithoutQueryFragment,
// OTel kind (explicit attribute so it appears in span data)
"otel.kind": "SERVER",
// Network attributes
"net.host.ip": localAddress,
"net.host.port": localPort,
"net.peer.ip": remoteAddress,
"net.peer.port": remotePort,
"sentry.http.prefetch": isKnownPrefetchRequest(request) || void 0,
// Old Semantic Conventions attributes for compatibility
"http.url": fullUrl,
"http.method": method,
"http.target": urlObj ? `${urlObj.pathname}${urlObj.search}` : httpTargetWithoutQueryFragment,
"http.host": host,
"net.host.name": hostname,
"http.client_ip": typeof ips === "string" ? ips.split(",")[0] : void 0,
"http.user_agent": userAgent,
"http.scheme": scheme,
"http.flavor": httpVersion,
"net.transport": httpVersion?.toUpperCase() === "QUIC" ? "ip_udp" : "ip_tcp",
...getRequestContentLengthAttribute(request),
...httpHeadersToSpanAttributes(
normalizedRequest.headers || {},
client.getOptions().sendDefaultPii ?? false
)
}
},
(span) => {
onSpanCreated?.(span, request, response);
let isEnded = false;
function endSpan(status) {
if (isEnded) {
return;
}
isEnded = true;
span.setAttributes({
"http.status_text": response.statusMessage?.toUpperCase(),
"http.response.status_code": response.statusCode,
"http.status_code": response.statusCode,
...httpHeadersToSpanAttributes(
headersToDict(response.headers),
client?.getOptions().sendDefaultPii ?? false,
"response"
)
});
span.setStatus(status);
onSpanEnd?.(span, request, response);
span.end();
}
response.once("close", () => {
endSpan(getSpanStatusFromHttpCode(response.statusCode));
});
response.once(errorMonitor, () => {
const httpStatus = getSpanStatusFromHttpCode(response.statusCode);
endSpan(httpStatus.code === SPAN_STATUS_ERROR ? httpStatus : { code: SPAN_STATUS_ERROR });
});
next();
}
);
}
};
}
function shouldIgnoreSpansForIncomingRequest(request, {
ignoreStaticAssets,
ignoreIncomingRequests
}) {
const urlPath = request.url;
const method = request.method?.toUpperCase();
if (method === "OPTIONS" || method === "HEAD" || !urlPath) {
return true;
}
if (ignoreStaticAssets && method === "GET" && isStaticAssetRequest(urlPath)) {
return true;
}
if (ignoreIncomingRequests?.(urlPath, request)) {
return true;
}
return false;
}
function isStaticAssetRequest(urlPath) {
const path = stripUrlQueryAndFragment(urlPath);
if (path.match(/\.(ico|png|jpg|jpeg|gif|svg|css|js|woff|woff2|ttf|eot|webp|avif)$/)) {
return true;
}
if (path.match(/^\/(robots\.txt|sitemap\.xml|manifest\.json|browserconfig\.xml)$/)) {
return true;
}
return false;
}
function isKnownPrefetchRequest(req) {
return req.headers["next-router-prefetch"] === "1";
}
function getRequestContentLengthAttribute(request) {
const { headers } = request;
const contentLengthHeader = headers["content-length"];
const length = contentLengthHeader ? parseInt(String(contentLengthHeader), 10) : -1;
const encoding = headers["content-encoding"];
return length >= 0 ? encoding && encoding !== "identity" ? { "http.request_content_length": length } : { "http.request_content_length_uncompressed": length } : {};
}
export { getHttpServerSubscriptions, instrumentServer, isStaticAssetRequest };
//# sourceMappingURL=server-subscription.js.map