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): Allow to pass instrumentation config to httpIntegration #12761

Merged
merged 2 commits into from
Jul 5, 2024
Merged
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
@@ -1,6 +1,6 @@
import { cleanupChildProcesses, createRunner } from '../../../utils/runner';

describe('express tracing experimental', () => {
describe('express tracing', () => {
afterAll(() => {
cleanupChildProcesses();
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
const Sentry = require('@sentry/node');

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
// disable attaching headers to /test/* endpoints
tracePropagationTargets: [/^(?!.*test).*$/],
tracesSampleRate: 1.0,
transport: loggingTransport,

integrations: [
Sentry.httpIntegration({
instrumentation: {
_experimentalConfig: {
serverName: 'sentry-test-server-name',
},
},
}),
],
});

// express must be required after Sentry is initialized
const express = require('express');
const cors = require('cors');
const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests');

const app = express();

app.use(cors());

app.get('/test', (_req, res) => {
res.send({ response: 'response 1' });
});

Sentry.setupExpressErrorHandler(app);

startExpressServerAndSendPortToRunner(app);
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
const Sentry = require('@sentry/node');

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
// disable attaching headers to /test/* endpoints
tracePropagationTargets: [/^(?!.*test).*$/],
tracesSampleRate: 1.0,
transport: loggingTransport,

integrations: [
Sentry.httpIntegration({
instrumentation: {
requestHook: (span, req) => {
span.setAttribute('attr1', 'yes');
Sentry.setExtra('requestHookCalled', {
url: req.url,
method: req.method,
});
},
responseHook: (span, res) => {
span.setAttribute('attr2', 'yes');
Sentry.setExtra('responseHookCalled', {
url: res.req.url,
method: res.req.method,
});
},
applyCustomAttributesOnSpan: (span, req, res) => {
span.setAttribute('attr3', 'yes');
Sentry.setExtra('applyCustomAttributesOnSpanCalled', {
reqUrl: req.url,
reqMethod: req.method,
resUrl: res.req.url,
resMethod: res.req.method,
});
},
},
}),
],
});

// express must be required after Sentry is initialized
const express = require('express');
const cors = require('cors');
const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests');

const app = express();

app.use(cors());

app.get('/test', (_req, res) => {
res.send({ response: 'response 1' });
});

Sentry.setupExpressErrorHandler(app);

startExpressServerAndSendPortToRunner(app);
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { cleanupChildProcesses, createRunner } from '../../../utils/runner';

describe('httpIntegration', () => {
afterAll(() => {
cleanupChildProcesses();
});

test('allows to pass instrumentation options to integration', done => {
// response shape seems different on Node 14, so we skip this there
const nodeMajorVersion = Number(process.versions.node.split('.')[0]);
if (nodeMajorVersion <= 14) {
done();
return;
}

createRunner(__dirname, 'server.js')
.ignore('session', 'sessions')
.expect({
transaction: {
contexts: {
trace: {
span_id: expect.any(String),
trace_id: expect.any(String),
data: {
url: expect.stringMatching(/\/test$/),
'http.response.status_code': 200,
attr1: 'yes',
attr2: 'yes',
attr3: 'yes',
},
op: 'http.server',
status: 'ok',
},
},
extra: {
requestHookCalled: {
url: expect.stringMatching(/\/test$/),
method: 'GET',
},
responseHookCalled: {
url: expect.stringMatching(/\/test$/),
method: 'GET',
},
applyCustomAttributesOnSpanCalled: {
reqUrl: expect.stringMatching(/\/test$/),
reqMethod: 'GET',
resUrl: expect.stringMatching(/\/test$/),
resMethod: 'GET',
},
},
},
})
.start(done)
.makeRequest('get', '/test');
});

test('allows to pass experimental config through to integration', done => {
createRunner(__dirname, 'server-experimental.js')
.ignore('session', 'sessions')
.expect({
transaction: {
contexts: {
trace: {
span_id: expect.any(String),
trace_id: expect.any(String),
data: {
url: expect.stringMatching(/\/test$/),
'http.response.status_code': 200,
'http.server_name': 'sentry-test-server-name',
},
op: 'http.server',
status: 'ok',
},
},
},
})
.start(done)
.makeRequest('get', '/test');
});
});
14 changes: 1 addition & 13 deletions packages/nextjs/src/server/httpIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,7 @@ class CustomNextjsHttpIntegration extends HttpInstrumentation {
}
}

interface HttpOptions {
/**
* Whether breadcrumbs should be recorded for requests.
* Defaults to true
*/
breadcrumbs?: boolean;

/**
* Do not capture spans or breadcrumbs for outgoing HTTP requests to URLs where the given callback returns `true`.
* This controls both span & breadcrumb creation - spans will be non recording if tracing is disabled.
*/
ignoreOutgoingRequests?: (url: string) => boolean;
}
type HttpOptions = Parameters<typeof originalHttpIntegration>[0];

/**
* The http integration instruments Node's internal http and https modules.
Expand Down
31 changes: 29 additions & 2 deletions packages/node/src/integrations/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,25 @@ interface HttpOptions {
*/
ignoreIncomingRequests?: (url: string) => boolean;

/**
* Additional instrumentation options that are passed to the underlying HttpInstrumentation.
*/
instrumentation?: {
requestHook?: (span: Span, req: ClientRequest | HTTPModuleRequestIncomingMessage) => void;
responseHook?: (span: Span, response: HTTPModuleRequestIncomingMessage | ServerResponse) => void;
applyCustomAttributesOnSpan?: (
span: Span,
request: ClientRequest | HTTPModuleRequestIncomingMessage,
response: HTTPModuleRequestIncomingMessage | ServerResponse,
) => void;

/**
* You can pass any configuration through to the underlying instrumention.
* Note that there are no semver guarantees for this!
*/
_experimentalConfig?: ConstructorParameters<typeof HttpInstrumentation>[0];
};

/** Allows to pass a custom version of HttpInstrumentation. We use this for Next.js. */
_instrumentation?: typeof HttpInstrumentation;
}
Expand All @@ -63,6 +82,7 @@ export const instrumentHttp = Object.assign(
const _InstrumentationClass = _httpOptions._instrumentation || HttpInstrumentation;

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

Expand Down Expand Up @@ -107,6 +127,7 @@ export const instrumentHttp = Object.assign(
// 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;
}

Expand Down Expand Up @@ -134,24 +155,30 @@ export const instrumentHttp = Object.assign(
const bestEffortTransactionName = `${httpMethod} ${httpTarget}`;

isolationScope.setTransactionName(bestEffortTransactionName);

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

_httpOptions.instrumentation?.responseHook?.(span, res);
},
applyCustomAttributesOnSpan: (
_span: Span,
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);
},
});

Expand Down
14 changes: 1 addition & 13 deletions packages/remix/src/utils/integrations/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,7 @@ class RemixHttpIntegration extends HttpInstrumentation {
}
}

interface HttpOptions {
/**
* Whether breadcrumbs should be recorded for requests.
* Defaults to true
*/
breadcrumbs?: boolean;

/**
* Do not capture spans or breadcrumbs for outgoing HTTP requests to URLs where the given callback returns `true`.
* This controls both span & breadcrumb creation - spans will be non recording if tracing is disabled.
*/
ignoreOutgoingRequests?: (url: string) => boolean;
}
type HttpOptions = Parameters<typeof originalHttpIntegration>[0];

/**
* The http integration instruments Node's internal http and https modules.
Expand Down
Loading