@sentry/core
Version:
Base implementation for all Sentry JavaScript SDKs
278 lines (275 loc) • 12.6 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, getContentLengthFromHeaders, httpRequestToRequestData, headersToDict } from '../../utils/request.js';
import { patchRequestToCaptureBody } from './patch-request-to-capture-body.js';
import { stripUrlQueryAndFragment, parseStringToURLObject, getUrlFragment, getUrlQuery } 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 { NETWORK_TRANSPORT, URL_FRAGMENT, URL_QUERY, URL_PATH, URL_FULL, SENTRY_HTTP_PREFETCH, URL_SCHEME, USER_AGENT_ORIGINAL, NETWORK_PROTOCOL_VERSION, NETWORK_PROTOCOL_NAME, HTTP_REQUEST_METHOD, NETWORK_PEER_PORT, NETWORK_PEER_ADDRESS, CLIENT_PORT, CLIENT_ADDRESS, NETWORK_LOCAL_PORT, NETWORK_LOCAL_ADDRESS, SERVER_PORT, SERVER_ADDRESS, SENTRY_KIND, SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, HTTP_RESPONSE_STATUS_CODE } from '@sentry/conventions/attributes';
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes.js';
import { getSpanStatusFromHttpCode, SPAN_STATUS_ERROR } from '../../tracing/spanstatus.js';
import { hasSpanStreamingEnabled } from '../../tracing/spans/hasSpanStreamingEnabled.js';
import { HTTP_SPAN_NAME_FALLBACK } from '../../tracing/spans/spanNames.js';
import { HTTP_SERVER } from '@sentry/conventions/op';
import { filterCollectedUrlQuery, filterCollectedUrl } from '../../utils/data-collection/filterCollectedUrl.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: configuredBodySize,
ignoreRequestBody,
sessions = true,
sessionFlushingDelayMS = 6e4
} = options;
const effectiveBodySize = configuredBodySize ?? (client.getDataCollectionOptions().httpBodies.includes("incomingRequest") ? "medium" : "none");
if (effectiveBodySize !== "none" && !ignoreRequestBody?.(url, request)) {
patchRequestToCaptureBody(request, isolationScope, effectiveBodySize, 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();
}
const dataCollectionOptions = client.getDataCollectionOptions();
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 = hasSpanStreamingEnabled(client) ? request.method?.toUpperCase() || HTTP_SPAN_NAME_FALLBACK : `${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 ?? {};
const collectClientAddress = client.getDataCollectionOptions().userInfo;
const clientAddress = getForwardedClientAddress(ips) ?? remoteAddress;
return startSpanManual(
{
name,
attributes: {
// Sentry-specific attributes
[SENTRY_OP]: HTTP_SERVER,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: "auto.http.server",
[SENTRY_SEGMENT_NAME_SOURCE]: "url",
[SENTRY_KIND]: "server",
// Network attributes
[SERVER_ADDRESS]: hostname,
[SERVER_PORT]: localPort,
[NETWORK_LOCAL_ADDRESS]: localAddress,
[NETWORK_LOCAL_PORT]: localPort,
[CLIENT_ADDRESS]: collectClientAddress ? clientAddress : void 0,
[CLIENT_PORT]: remotePort,
[NETWORK_PEER_ADDRESS]: collectClientAddress ? remoteAddress : void 0,
[NETWORK_PEER_PORT]: remotePort,
[SENTRY_HTTP_PREFETCH]: isKnownPrefetchRequest(request) || void 0,
[URL_FULL]: filterCollectedUrl(fullUrl, client),
[URL_PATH]: urlObj?.pathname ?? httpTargetWithoutQueryFragment,
[URL_QUERY]: filterCollectedUrlQuery(getUrlQuery(urlObj?.search), client),
[URL_FRAGMENT]: getUrlFragment(urlObj?.hash),
[HTTP_REQUEST_METHOD]: method,
[NETWORK_PROTOCOL_NAME]: "http",
[NETWORK_PROTOCOL_VERSION]: httpVersion,
[USER_AGENT_ORIGINAL]: userAgent,
[URL_SCHEME]: scheme,
[NETWORK_TRANSPORT]: httpVersion?.toUpperCase() === "QUIC" ? "udp" : "tcp",
"http.request.body.size": getContentLengthFromHeaders(request.headers),
...httpHeadersToSpanAttributes(normalizedRequest.headers || {}, dataCollectionOptions)
}
},
(span) => {
onSpanCreated?.(span, request, response);
let isEnded = false;
function endSpan(status) {
if (isEnded) {
return;
}
isEnded = true;
span.setAttributes({
"http.response.status_text": response.statusMessage?.toUpperCase(),
[HTTP_RESPONSE_STATUS_CODE]: response.statusCode,
...httpHeadersToSpanAttributes(headersToDict(response.headers), dataCollectionOptions, "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 getForwardedClientAddress(forwardedFor) {
return typeof forwardedFor === "string" ? forwardedFor.split(",")[0]?.trim() || void 0 : void 0;
}
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";
}
export { getHttpServerSubscriptions, instrumentServer, isStaticAssetRequest };
//# sourceMappingURL=server-subscription.js.map