diff --git a/packages/node-integration-tests/package.json b/packages/node-integration-tests/package.json index 748748312d89..0f85c198b985 100644 --- a/packages/node-integration-tests/package.json +++ b/packages/node-integration-tests/package.json @@ -18,6 +18,7 @@ "@types/mongodb": "^3.6.20", "@types/mysql": "^2.15.21", "@types/pg": "^8.6.5", + "cors": "^2.8.5", "express": "^4.17.3", "mongodb": "^3.7.3", "mongodb-memory-server": "^7.6.3", diff --git a/packages/node-integration-tests/suites/express/handle-error/server.ts b/packages/node-integration-tests/suites/express/handle-error/server.ts new file mode 100644 index 000000000000..50085cc5a41b --- /dev/null +++ b/packages/node-integration-tests/suites/express/handle-error/server.ts @@ -0,0 +1,19 @@ +import * as Sentry from '@sentry/node'; +import express from 'express'; + +const app = express(); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', +}); + +app.use(Sentry.Handlers.requestHandler()); + +app.get('/test/express', () => { + throw new Error('test_error'); +}); + +app.use(Sentry.Handlers.errorHandler()); + +export default app; diff --git a/packages/node-integration-tests/suites/express/handle-error/test.ts b/packages/node-integration-tests/suites/express/handle-error/test.ts new file mode 100644 index 000000000000..66cc1ec2d9ae --- /dev/null +++ b/packages/node-integration-tests/suites/express/handle-error/test.ts @@ -0,0 +1,23 @@ +import { assertSentryEvent, getEventRequest, runServer } from '../../../utils/index'; + +test('should capture and send Express controller error.', async () => { + const url = await runServer(__dirname, `${__dirname}/server.ts`); + const event = await getEventRequest(`${url}/express`); + + expect((event as any).exception.values[0].stacktrace.frames.length).toBeGreaterThan(0); + + assertSentryEvent(event, { + exception: { + values: [ + { + mechanism: { + type: 'generic', + handled: true, + }, + type: 'Error', + value: 'test_error', + }, + ], + }, + }); +}); diff --git a/packages/node-integration-tests/suites/express/sentry-trace/server.ts b/packages/node-integration-tests/suites/express/sentry-trace/server.ts new file mode 100644 index 000000000000..172c2edd3900 --- /dev/null +++ b/packages/node-integration-tests/suites/express/sentry-trace/server.ts @@ -0,0 +1,30 @@ +import * as Sentry from '@sentry/node'; +import * as Tracing from '@sentry/tracing'; +import cors from 'cors'; +import express from 'express'; +import http from 'http'; + +const app = express(); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + integrations: [new Sentry.Integrations.Http({ tracing: true }), new Tracing.Integrations.Express({ app })], + tracesSampleRate: 1.0, +}); + +app.use(Sentry.Handlers.requestHandler()); +app.use(Sentry.Handlers.tracingHandler()); + +app.use(cors()); + +app.get('/test/express', (_req, res) => { + const headers = http.get('http://somewhere.not.sentry/').getHeaders(); + + // Responding with the headers outgoing request headers back to the assertions. + res.send({ test_data: headers }); +}); + +app.use(Sentry.Handlers.errorHandler()); + +export default app; diff --git a/packages/node-integration-tests/suites/express/sentry-trace/trace-header-assign/test.ts b/packages/node-integration-tests/suites/express/sentry-trace/trace-header-assign/test.ts new file mode 100644 index 000000000000..5ee98789c299 --- /dev/null +++ b/packages/node-integration-tests/suites/express/sentry-trace/trace-header-assign/test.ts @@ -0,0 +1,22 @@ +import { TRACEPARENT_REGEXP } from '@sentry/utils'; + +import { getAPIResponse, runServer } from '../../../../utils/index'; +import path = require('path'); + +test('Should assign `sentry-trace` header which sets parent trace id of an outgoing request.', async () => { + const url = await runServer(__dirname, `${path.resolve(__dirname, '..')}/server.ts`); + + const response = await getAPIResponse(new URL(`${url}/express`), { + 'sentry-trace': '12312012123120121231201212312012-1121201211212012-0', + }); + + expect(response).toBeDefined(); + expect(response).toMatchObject({ + test_data: { + host: 'somewhere.not.sentry', + 'sentry-trace': expect.stringContaining('12312012123120121231201212312012-'), + }, + }); + + expect(TRACEPARENT_REGEXP.test(response.test_data['sentry-trace'])).toBe(true); +}); diff --git a/packages/node-integration-tests/suites/express/sentry-trace/trace-header-out/test.ts b/packages/node-integration-tests/suites/express/sentry-trace/trace-header-out/test.ts new file mode 100644 index 000000000000..ca7eb56fd61c --- /dev/null +++ b/packages/node-integration-tests/suites/express/sentry-trace/trace-header-out/test.ts @@ -0,0 +1,20 @@ +import { TRACEPARENT_REGEXP } from '@sentry/utils'; + +import { getAPIResponse, runServer } from '../../../../utils/index'; +import path = require('path'); + +test('should attach a `sentry-trace` header to an outgoing request.', async () => { + const url = await runServer(__dirname, `${path.resolve(__dirname, '..')}/server.ts`); + + const response = await getAPIResponse(new URL(`${url}/express`)); + + expect(response).toBeDefined(); + expect(response).toMatchObject({ + test_data: { + host: 'somewhere.not.sentry', + 'sentry-trace': expect.any(String), + }, + }); + + expect(TRACEPARENT_REGEXP.test(response.test_data['sentry-trace'])).toBe(true); +}); diff --git a/packages/node-integration-tests/suites/express/tracing/server.ts b/packages/node-integration-tests/suites/express/tracing/server.ts new file mode 100644 index 000000000000..1eac2074ddaa --- /dev/null +++ b/packages/node-integration-tests/suites/express/tracing/server.ts @@ -0,0 +1,26 @@ +import * as Sentry from '@sentry/node'; +import * as Tracing from '@sentry/tracing'; +import cors from 'cors'; +import express from 'express'; + +const app = express(); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + integrations: [new Sentry.Integrations.Http({ tracing: true }), new Tracing.Integrations.Express({ app })], + tracesSampleRate: 1.0, +}); + +app.use(Sentry.Handlers.requestHandler()); +app.use(Sentry.Handlers.tracingHandler()); + +app.use(cors()); + +app.get('/test/express', (_req, res) => { + res.send({ response: 'response 1' }); +}); + +app.use(Sentry.Handlers.errorHandler()); + +export default app; diff --git a/packages/node-integration-tests/suites/express/tracing/test.ts b/packages/node-integration-tests/suites/express/tracing/test.ts new file mode 100644 index 000000000000..3af4c6fa0238 --- /dev/null +++ b/packages/node-integration-tests/suites/express/tracing/test.ts @@ -0,0 +1,29 @@ +import { assertSentryTransaction, getEnvelopeRequest, runServer } from '../../../utils/index'; + +test('should create and send transactions for Express routes and spans for middlewares.', async () => { + const url = await runServer(__dirname, `${__dirname}/server.ts`); + const envelope = await getEnvelopeRequest(`${url}/express`); + + expect(envelope).toHaveLength(3); + + assertSentryTransaction(envelope[2], { + contexts: { + trace: { + data: { + url: '/test/express', + }, + op: 'http.server', + status: 'ok', + tags: { + 'http.status_code': '200', + }, + }, + }, + spans: [ + { + description: 'corsMiddleware', + op: 'express.middleware.use', + }, + ], + }); +}); diff --git a/packages/node-integration-tests/utils/index.ts b/packages/node-integration-tests/utils/index.ts index c861e235066d..f458ede19153 100644 --- a/packages/node-integration-tests/utils/index.ts +++ b/packages/node-integration-tests/utils/index.ts @@ -1,6 +1,8 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { parseSemver } from '@sentry/utils'; import { Express } from 'express'; import * as http from 'http'; +import { RequestOptions } from 'https'; import nock from 'nock'; import * as path from 'path'; import { getPortPromise } from 'portfinder'; @@ -130,6 +132,39 @@ export const getMultipleEnvelopeRequest = async (url: string, count: number): Pr }); }; +/** + * Sends a get request to given URL, with optional headers + * + * @param {URL} url + * @param {Record} [headers] + * @return {*} {Promise} + */ +export const getAPIResponse = async (url: URL, headers?: Record): Promise => { + return await new Promise(resolve => { + http.get( + headers + ? ({ + protocol: url.protocol, + host: url.hostname, + path: url.pathname, + port: url.port, + headers, + } as RequestOptions) + : url, + response => { + let body = ''; + + response.on('data', function (chunk: string) { + body += chunk; + }); + response.on('end', function () { + resolve(JSON.parse(body)); + }); + }, + ); + }); +}; + /** * Intercepts and extracts a single request containing a Sentry envelope * diff --git a/yarn.lock b/yarn.lock index 378af0b069b2..82a6bcf3a04f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4573,11 +4573,32 @@ after@0.8.2: resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8= -agent-base@4, agent-base@5, agent-base@6, agent-base@^4.3.0, agent-base@^6.0.2, agent-base@~4.2.1: +agent-base@4, agent-base@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" + integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== + dependencies: + es6-promisify "^5.0.0" + +agent-base@5: version "5.1.1" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c" integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g== +agent-base@6, agent-base@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +agent-base@~4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" + integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg== + dependencies: + es6-promisify "^5.0.0" + agentkeepalive@^3.4.1: version "3.5.2" resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-3.5.2.tgz#a113924dd3fa24a0bc3b78108c450c2abee00f67" @@ -8032,7 +8053,7 @@ core-util-is@1.0.2, core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= -cors@~2.8.5: +cors@^2.8.5, cors@~2.8.5: version "2.8.5" resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== @@ -10080,6 +10101,18 @@ es6-object-assign@^1.1.0: resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c" integrity sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw= +es6-promise@^4.0.3: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= + dependencies: + es6-promise "^4.0.3" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -13594,7 +13627,7 @@ jest-environment-jsdom@^24.9.0: jest-util "^24.9.0" jsdom "^11.5.1" -jest-environment-node@24, "jest-environment-node@>=24 <=26", jest-environment-node@^24.9.0: +"jest-environment-node@>=24 <=26", jest-environment-node@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-24.9.0.tgz#333d2d2796f9687f2aeebf0742b519f33c1cbfd3" integrity sha512-6d4V2f4nxzIzwendo27Tr0aFm+IXWa0XEUnaH6nU0FMaozxovt+sfRvh4J47wL1OvF83I3SSTu0XK+i4Bqe7uA==