Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(node): Expose getHttpInstrumentationOptions utility #13719

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const NODE_EXPORTS_IGNORE = [
'SentryContextManager',
'validateOpenTelemetrySetup',
'preloadOpenTelemetry',
'getHttpInstrumentationOptions',
];

const nodeExports = Object.keys(SentryNode).filter(e => !NODE_EXPORTS_IGNORE.includes(e));
Expand Down
2 changes: 1 addition & 1 deletion packages/node/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { httpIntegration } from './integrations/http';
export { httpIntegration, getHttpInstrumentationOptions } from './integrations/http';
export { nativeNodeFetchIntegration } from './integrations/node-fetch';
export { fsIntegration } from './integrations/fs';

Expand Down
209 changes: 110 additions & 99 deletions packages/node/src/integrations/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,105 +94,7 @@ export const instrumentHttp = Object.assign(

_httpInstrumentation = new _InstrumentationClass({
..._httpOptions.instrumentation?._experimentalConfig,
ignoreOutgoingRequestHook: request => {
const url = getRequestUrl(request);

if (!url) {
return false;
}

const _ignoreOutgoingRequests = _httpOptions.ignoreOutgoingRequests;
if (_ignoreOutgoingRequests && _ignoreOutgoingRequests(url, request)) {
return true;
}

return false;
},

ignoreIncomingRequestHook: request => {
// request.url is the only property that holds any information about the url
// it only consists of the URL path and query string (if any)
const urlPath = request.url;

const method = request.method?.toUpperCase();
// We do not capture OPTIONS/HEAD requests as transactions
if (method === 'OPTIONS' || method === 'HEAD') {
return true;
}

const _ignoreIncomingRequests = _httpOptions.ignoreIncomingRequests;
if (urlPath && _ignoreIncomingRequests && _ignoreIncomingRequests(urlPath, request)) {
return true;
}

return false;
},

requireParentforOutgoingSpans: false,
requireParentforIncomingSpans: false,
requestHook: (span, req) => {
addOriginToSpan(span, 'auto.http.otel.http');

// both, incoming requests and "client" requests made within the app trigger the requestHook
// we only want to isolate and further annotate incoming requests (IncomingMessage)
if (_isClientRequest(req)) {
_httpOptions.instrumentation?.requestHook?.(span, req);
return;
}

const scopes = getCapturedScopesOnSpan(span);

const isolationScope = (scopes.isolationScope || getIsolationScope()).clone();
const scope = scopes.scope || getCurrentScope();

// Update the isolation scope, isolate this request
isolationScope.setSDKProcessingMetadata({ request: req });

const client = getClient<NodeClient>();
if (client && client.getOptions().autoSessionTracking) {
isolationScope.setRequestSession({ status: 'ok' });
}
setIsolationScope(isolationScope);
setCapturedScopesOnSpan(span, scope, isolationScope);

// attempt to update the scope's `transactionName` based on the request URL
// Ideally, framework instrumentations coming after the HttpInstrumentation
// update the transactionName once we get a parameterized route.
const httpMethod = (req.method || 'GET').toUpperCase();
const httpTarget = stripUrlQueryAndFragment(req.url || '/');

const bestEffortTransactionName = `${httpMethod} ${httpTarget}`;

isolationScope.setTransactionName(bestEffortTransactionName);

if (isKnownPrefetchRequest(req)) {
span.setAttribute('sentry.http.prefetch', true);
}

_httpOptions.instrumentation?.requestHook?.(span, req);
},
responseHook: (span, res) => {
const client = getClient<NodeClient>();
if (client && client.getOptions().autoSessionTracking) {
setImmediate(() => {
client['_captureRequestSession']();
});
}

_httpOptions.instrumentation?.responseHook?.(span, res);
},
applyCustomAttributesOnSpan: (
span: Span,
request: ClientRequest | HTTPModuleRequestIncomingMessage,
response: HTTPModuleRequestIncomingMessage | ServerResponse,
) => {
const _breadcrumbs = typeof _httpOptions.breadcrumbs === 'undefined' ? true : _httpOptions.breadcrumbs;
if (_breadcrumbs) {
_addRequestBreadcrumb(request, response);
}

_httpOptions.instrumentation?.applyCustomAttributesOnSpan?.(span, request, response);
},
...getHttpInstrumentationOptions(),
});

addOpenTelemetryInstrumentation(_httpInstrumentation);
Expand All @@ -218,6 +120,115 @@ const _httpIntegration = ((options: HttpOptions = {}) => {
*/
export const httpIntegration = defineIntegration(_httpIntegration);

/**
* Get the options to be passed to the HTTP instrumentation.
* This includes all the things you need to make the Sentry SDK work,
* especially `requestHook` is necessary to be passed.
*/
export function getHttpInstrumentationOptions(): ConstructorParameters<typeof HttpInstrumentation>[0] {
return {
ignoreOutgoingRequestHook: request => {
const url = getRequestUrl(request);

if (!url) {
return false;
}

const _ignoreOutgoingRequests = _httpOptions.ignoreOutgoingRequests;
if (_ignoreOutgoingRequests && _ignoreOutgoingRequests(url, request)) {
return true;
}

return false;
},

ignoreIncomingRequestHook: request => {
// request.url is the only property that holds any information about the url
// it only consists of the URL path and query string (if any)
const urlPath = request.url;

const method = request.method?.toUpperCase();
// We do not capture OPTIONS/HEAD requests as transactions
if (method === 'OPTIONS' || method === 'HEAD') {
return true;
}

const _ignoreIncomingRequests = _httpOptions.ignoreIncomingRequests;
if (urlPath && _ignoreIncomingRequests && _ignoreIncomingRequests(urlPath, request)) {
return true;
}

return false;
},

requireParentforOutgoingSpans: false,
requireParentforIncomingSpans: false,
requestHook: (span, req) => {
addOriginToSpan(span, 'auto.http.otel.http');

// both, incoming requests and "client" requests made within the app trigger the requestHook
// we only want to isolate and further annotate incoming requests (IncomingMessage)
if (_isClientRequest(req)) {
_httpOptions.instrumentation?.requestHook?.(span, req);
return;
}

const scopes = getCapturedScopesOnSpan(span);

const isolationScope = (scopes.isolationScope || getIsolationScope()).clone();
const scope = scopes.scope || getCurrentScope();

// Update the isolation scope, isolate this request
isolationScope.setSDKProcessingMetadata({ request: req });

const client = getClient<NodeClient>();
if (client && client.getOptions().autoSessionTracking) {
isolationScope.setRequestSession({ status: 'ok' });
}
setIsolationScope(isolationScope);
setCapturedScopesOnSpan(span, scope, isolationScope);

// attempt to update the scope's `transactionName` based on the request URL
// Ideally, framework instrumentations coming after the HttpInstrumentation
// update the transactionName once we get a parameterized route.
const httpMethod = (req.method || 'GET').toUpperCase();
const httpTarget = stripUrlQueryAndFragment(req.url || '/');

const bestEffortTransactionName = `${httpMethod} ${httpTarget}`;

isolationScope.setTransactionName(bestEffortTransactionName);

if (isKnownPrefetchRequest(req)) {
span.setAttribute('sentry.http.prefetch', true);
}

_httpOptions.instrumentation?.requestHook?.(span, req);
},
responseHook: (span, res) => {
const client = getClient<NodeClient>();
if (client && client.getOptions().autoSessionTracking) {
setImmediate(() => {
client['_captureRequestSession']();
});
}

_httpOptions.instrumentation?.responseHook?.(span, res);
},
applyCustomAttributesOnSpan: (
span: Span,
request: ClientRequest | HTTPModuleRequestIncomingMessage,
response: HTTPModuleRequestIncomingMessage | ServerResponse,
) => {
const _breadcrumbs = typeof _httpOptions.breadcrumbs === 'undefined' ? true : _httpOptions.breadcrumbs;
if (_breadcrumbs) {
_addRequestBreadcrumb(request, response);
}

_httpOptions.instrumentation?.applyCustomAttributesOnSpan?.(span, request, response);
},
};
}

/** Add a breadcrumb for outgoing requests. */
function _addRequestBreadcrumb(
request: ClientRequest | HTTPModuleRequestIncomingMessage,
Expand Down
Loading