diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index b424b26acf70..2463a941f825 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -49,7 +49,7 @@ }, "devDependencies": { "@types/glob": "8.0.0", - "@types/node": "^14.6.4", + "@types/node": "^14.18.0", "@types/pako": "^2.0.0", "glob": "8.0.3" }, diff --git a/dev-packages/e2e-tests/package.json b/dev-packages/e2e-tests/package.json index 5054482ed6ad..34c3a95881b2 100644 --- a/dev-packages/e2e-tests/package.json +++ b/dev-packages/e2e-tests/package.json @@ -18,15 +18,17 @@ }, "devDependencies": { "@types/glob": "8.0.0", - "@types/node": "^14.6.4", + "@types/node": "^14.18.0", "dotenv": "16.0.3", "esbuild": "0.20.0", "glob": "8.0.3", "ts-node": "10.9.1", - "yaml": "2.2.2" + "yaml": "2.2.2", + "rimraf": "^3.0.2" }, "volta": { "node": "18.18.0", - "yarn": "1.22.19" + "yarn": "1.22.19", + "pnpm": "8.15.5" } } diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-app/src/app.js b/dev-packages/e2e-tests/test-applications/node-fastify-app/src/app.js index 7135fb33d91a..ded1a2de7fcb 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-app/src/app.js +++ b/dev-packages/e2e-tests/test-applications/node-fastify-app/src/app.js @@ -6,6 +6,7 @@ const http = require('http'); const app = fastify(); const port = 3030; +const port2 = 3040; Sentry.setupFastifyErrorHandler(app); @@ -17,20 +18,22 @@ app.get('/test-param/:param', function (req, res) { res.send({ paramWas: req.params.param }); }); -app.get('/test-inbound-headers', function (req, res) { +app.get('/test-inbound-headers/:id', function (req, res) { const headers = req.headers; - res.send({ headers }); + res.send({ headers, id: req.params.id }); }); -app.get('/test-outgoing-http', async function (req, res) { - const data = await makeHttpRequest('http://localhost:3030/test-inbound-headers'); +app.get('/test-outgoing-http/:id', async function (req, res) { + const id = req.params.id; + const data = await makeHttpRequest(`http://localhost:3030/test-inbound-headers/${id}`); res.send(data); }); -app.get('/test-outgoing-fetch', async function (req, res) { - const response = await fetch('http://localhost:3030/test-inbound-headers'); +app.get('/test-outgoing-fetch/:id', async function (req, res) { + const id = req.params.id; + const response = await fetch(`http://localhost:3030/test-inbound-headers/${id}`); const data = await response.json(); res.send(data); @@ -56,8 +59,48 @@ app.get('/test-exception', async function (req, res) { throw new Error('This is an exception'); }); +app.get('/test-outgoing-fetch-external-allowed', async function (req, res) { + const fetchResponse = await fetch(`http://localhost:${port2}/external-allowed`); + const data = await fetchResponse.json(); + + res.send(data); +}); + +app.get('/test-outgoing-fetch-external-disallowed', async function (req, res) { + const fetchResponse = await fetch(`http://localhost:${port2}/external-disallowed`); + const data = await fetchResponse.json(); + + res.send(data); +}); + +app.get('/test-outgoing-http-external-allowed', async function (req, res) { + const data = await makeHttpRequest(`http://localhost:${port2}/external-allowed`); + res.send(data); +}); + +app.get('/test-outgoing-http-external-disallowed', async function (req, res) { + const data = await makeHttpRequest(`http://localhost:${port2}/external-disallowed`); + res.send(data); +}); + app.listen({ port: port }); +// A second app so we can test header propagation between external URLs +const app2 = fastify(); +app2.get('/external-allowed', function (req, res) { + const headers = req.headers; + + res.send({ headers, route: '/external-allowed' }); +}); + +app2.get('/external-disallowed', function (req, res) { + const headers = req.headers; + + res.send({ headers, route: '/external-disallowed' }); +}); + +app2.listen({ port: port2 }); + function makeHttpRequest(url) { return new Promise(resolve => { const data = []; @@ -67,9 +110,16 @@ function makeHttpRequest(url) { httpRes.on('data', chunk => { data.push(chunk); }); + httpRes.on('error', error => { + resolve({ error: error.message, url }); + }); httpRes.on('end', () => { - const json = JSON.parse(Buffer.concat(data).toString()); - resolve(json); + try { + const json = JSON.parse(Buffer.concat(data).toString()); + resolve(json); + } catch { + resolve({ data: Buffer.concat(data).toString(), url }); + } }); }) .end(); diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-app/src/tracing.js b/dev-packages/e2e-tests/test-applications/node-fastify-app/src/tracing.js index 4cf352cda681..136b401cbd73 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-app/src/tracing.js +++ b/dev-packages/e2e-tests/test-applications/node-fastify-app/src/tracing.js @@ -6,4 +6,5 @@ Sentry.init({ integrations: [], tracesSampleRate: 1, tunnel: 'http://localhost:3031/', // proxy server + tracePropagationTargets: ['http://localhost:3030', '/external-allowed'], }); diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/propagation.test.ts index 89a6320f725a..411863f54cfb 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/propagation.test.ts @@ -1,32 +1,33 @@ +import crypto from 'crypto'; import { expect, test } from '@playwright/test'; -import { Span } from '@sentry/types'; +import { SpanJSON } from '@sentry/types'; import axios from 'axios'; import { waitForTransaction } from '../event-proxy-server'; test('Propagates trace for outgoing http requests', async ({ baseURL }) => { + const id = crypto.randomUUID(); + const inboundTransactionPromise = waitForTransaction('node-fastify-app', transactionEvent => { return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent?.transaction === 'GET /test-inbound-headers' + transactionEvent.contexts?.trace?.op === 'http.server' && + transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-inbound-headers/${id}` ); }); const outboundTransactionPromise = waitForTransaction('node-fastify-app', transactionEvent => { return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent?.transaction === 'GET /test-outgoing-http' + transactionEvent.contexts?.trace?.op === 'http.server' && + transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-http/${id}` ); }); - const { data } = await axios.get(`${baseURL}/test-outgoing-http`); + const { data } = await axios.get(`${baseURL}/test-outgoing-http/${id}`); const inboundTransaction = await inboundTransactionPromise; const outboundTransaction = await outboundTransactionPromise; const traceId = outboundTransaction?.contexts?.trace?.trace_id; - const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client') as - | ReturnType - | undefined; + const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client') as SpanJSON | undefined; expect(outgoingHttpSpan).toBeDefined(); @@ -56,15 +57,15 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { 'sentry.origin': 'auto.http.otel.http', 'sentry.op': 'http.server', 'sentry.sample_rate': 1, - url: 'http://localhost:3030/test-outgoing-http', + url: `http://localhost:3030/test-outgoing-http/${id}`, 'otel.kind': 'SERVER', 'http.response.status_code': 200, - 'http.url': 'http://localhost:3030/test-outgoing-http', + 'http.url': `http://localhost:3030/test-outgoing-http/${id}`, 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', 'http.scheme': 'http', - 'http.target': '/test-outgoing-http', + 'http.target': `/test-outgoing-http/${id}`, 'http.user_agent': 'axios/1.6.7', 'http.flavor': '1.1', 'net.transport': 'ip_tcp', @@ -74,7 +75,7 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { 'net.peer.port': expect.any(Number), 'http.status_code': 200, 'http.status_text': 'OK', - 'http.route': '/test-outgoing-http', + 'http.route': '/test-outgoing-http/:id', }, op: 'http.server', span_id: expect.any(String), @@ -89,15 +90,15 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { 'sentry.origin': 'auto.http.otel.http', 'sentry.op': 'http.server', 'sentry.sample_rate': 1, - url: 'http://localhost:3030/test-inbound-headers', + url: `http://localhost:3030/test-inbound-headers/${id}`, 'otel.kind': 'SERVER', 'http.response.status_code': 200, - 'http.url': 'http://localhost:3030/test-inbound-headers', + 'http.url': `http://localhost:3030/test-inbound-headers/${id}`, 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', 'http.scheme': 'http', - 'http.target': '/test-inbound-headers', + 'http.target': `/test-inbound-headers/${id}`, 'http.flavor': '1.1', 'net.transport': 'ip_tcp', 'net.host.ip': expect.any(String), @@ -106,7 +107,7 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { 'net.peer.port': expect.any(Number), 'http.status_code': 200, 'http.status_text': 'OK', - 'http.route': '/test-inbound-headers', + 'http.route': '/test-inbound-headers/:id', }, op: 'http.server', parent_span_id: outgoingHttpSpanId, @@ -118,29 +119,29 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { }); test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { + const id = crypto.randomUUID(); + const inboundTransactionPromise = waitForTransaction('node-fastify-app', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent?.transaction === 'GET /test-inbound-headers' + transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-inbound-headers/${id}` ); }); const outboundTransactionPromise = waitForTransaction('node-fastify-app', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent?.transaction === 'GET /test-outgoing-fetch' + transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-fetch/${id}` ); }); - const { data } = await axios.get(`${baseURL}/test-outgoing-fetch`); + const { data } = await axios.get(`${baseURL}/test-outgoing-fetch/${id}`); const inboundTransaction = await inboundTransactionPromise; const outboundTransaction = await outboundTransactionPromise; const traceId = outboundTransaction?.contexts?.trace?.trace_id; - const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client') as - | ReturnType - | undefined; + const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client') as SpanJSON | undefined; expect(outgoingHttpSpan).toBeDefined(); @@ -170,15 +171,15 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { 'sentry.origin': 'auto.http.otel.http', 'sentry.op': 'http.server', 'sentry.sample_rate': 1, - url: 'http://localhost:3030/test-outgoing-fetch', + url: `http://localhost:3030/test-outgoing-fetch/${id}`, 'otel.kind': 'SERVER', 'http.response.status_code': 200, - 'http.url': 'http://localhost:3030/test-outgoing-fetch', + 'http.url': `http://localhost:3030/test-outgoing-fetch/${id}`, 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', 'http.scheme': 'http', - 'http.target': '/test-outgoing-fetch', + 'http.target': `/test-outgoing-fetch/${id}`, 'http.user_agent': 'axios/1.6.7', 'http.flavor': '1.1', 'net.transport': 'ip_tcp', @@ -188,7 +189,7 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { 'net.peer.port': expect.any(Number), 'http.status_code': 200, 'http.status_text': 'OK', - 'http.route': '/test-outgoing-fetch', + 'http.route': '/test-outgoing-fetch/:id', }, op: 'http.server', span_id: expect.any(String), @@ -203,15 +204,15 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { 'sentry.origin': 'auto.http.otel.http', 'sentry.op': 'http.server', 'sentry.sample_rate': 1, - url: 'http://localhost:3030/test-inbound-headers', + url: `http://localhost:3030/test-inbound-headers/${id}`, 'otel.kind': 'SERVER', 'http.response.status_code': 200, - 'http.url': 'http://localhost:3030/test-inbound-headers', + 'http.url': `http://localhost:3030/test-inbound-headers/${id}`, 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', 'http.scheme': 'http', - 'http.target': '/test-inbound-headers', + 'http.target': `/test-inbound-headers/${id}`, 'http.flavor': '1.1', 'net.transport': 'ip_tcp', 'net.host.ip': expect.any(String), @@ -220,7 +221,7 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { 'net.peer.port': expect.any(Number), 'http.status_code': 200, 'http.status_text': 'OK', - 'http.route': '/test-inbound-headers', + 'http.route': '/test-inbound-headers/:id', }), op: 'http.server', parent_span_id: outgoingHttpSpanId, @@ -230,3 +231,121 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { origin: 'auto.http.otel.http', }); }); + +test('Propagates trace for outgoing external http requests', async ({ baseURL }) => { + const inboundTransactionPromise = waitForTransaction('node-fastify-app', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-http-external-allowed` + ); + }); + + const { data } = await axios.get(`${baseURL}/test-outgoing-http-external-allowed`); + + const inboundTransaction = await inboundTransactionPromise; + + const traceId = inboundTransaction?.contexts?.trace?.trace_id; + const spanId = inboundTransaction?.spans?.find(span => span.op === 'http.client')?.span_id; + + expect(traceId).toEqual(expect.any(String)); + expect(spanId).toEqual(expect.any(String)); + + expect(data).toEqual({ + route: '/external-allowed', + headers: expect.objectContaining({ + 'sentry-trace': `${traceId}-${spanId}-1`, + baggage: expect.any(String), + }), + }); + + const baggage = (data.headers.baggage || '').split(','); + expect(baggage).toEqual( + expect.arrayContaining([ + 'sentry-environment=qa', + `sentry-trace_id=${traceId}`, + expect.stringMatching(/sentry-public_key=/), + ]), + ); +}); + +test('Does not propagate outgoing http requests not covered by tracePropagationTargets', async ({ baseURL }) => { + const inboundTransactionPromise = waitForTransaction('node-fastify-app', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-http-external-disallowed` + ); + }); + + const { data } = await axios.get(`${baseURL}/test-outgoing-http-external-disallowed`); + + const inboundTransaction = await inboundTransactionPromise; + + const traceId = inboundTransaction?.contexts?.trace?.trace_id; + const spanId = inboundTransaction?.spans?.find(span => span.op === 'http.client')?.span_id; + + expect(traceId).toEqual(expect.any(String)); + expect(spanId).toEqual(expect.any(String)); + + expect(data.route).toBe('/external-disallowed'); + expect(data.headers?.['sentry-trace']).toBeUndefined(); + expect(data.headers?.baggage).toBeUndefined(); +}); + +test('Propagates trace for outgoing external fetch requests', async ({ baseURL }) => { + const inboundTransactionPromise = waitForTransaction('node-fastify-app', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-fetch-external-allowed` + ); + }); + + const { data } = await axios.get(`${baseURL}/test-outgoing-fetch-external-allowed`); + + const inboundTransaction = await inboundTransactionPromise; + + const traceId = inboundTransaction?.contexts?.trace?.trace_id; + const spanId = inboundTransaction?.spans?.find(span => span.op === 'http.client')?.span_id; + + expect(traceId).toEqual(expect.any(String)); + expect(spanId).toEqual(expect.any(String)); + + expect(data).toEqual({ + route: '/external-allowed', + headers: expect.objectContaining({ + 'sentry-trace': `${traceId}-${spanId}-1`, + baggage: expect.any(String), + }), + }); + + const baggage = (data.headers.baggage || '').split(','); + expect(baggage).toEqual( + expect.arrayContaining([ + 'sentry-environment=qa', + `sentry-trace_id=${traceId}`, + expect.stringMatching(/sentry-public_key=/), + ]), + ); +}); + +test('Does not propagate outgoing fetch requests not covered by tracePropagationTargets', async ({ baseURL }) => { + const inboundTransactionPromise = waitForTransaction('node-fastify-app', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-fetch-external-disallowed` + ); + }); + + const { data } = await axios.get(`${baseURL}/test-outgoing-fetch-external-disallowed`); + + const inboundTransaction = await inboundTransactionPromise; + + const traceId = inboundTransaction?.contexts?.trace?.trace_id; + const spanId = inboundTransaction?.spans?.find(span => span.op === 'http.client')?.span_id; + + expect(traceId).toEqual(expect.any(String)); + expect(spanId).toEqual(expect.any(String)); + + expect(data.route).toBe('/external-disallowed'); + expect(data.headers?.['sentry-trace']).toBeUndefined(); + expect(data.headers?.baggage).toBeUndefined(); +}); diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts index 3a8d904de3c9..e2aa6c405773 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts @@ -84,7 +84,7 @@ conditionalTest({ min: 18 })('LocalVariables integration', () => { child.on('message', msg => { reportedCount++; - const rssMb = msg.memUsage.rss / 1024 / 1024; + const rssMb = (msg as { memUsage: { rss: number } }).memUsage.rss / 1024 / 1024; // We shouldn't use more than 120MB of memory expect(rssMb).toBeLessThan(120); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargetsDisabled/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargetsDisabled/scenario.ts deleted file mode 100644 index cfad7894d2b8..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargetsDisabled/scenario.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as http from 'http'; -import * as Sentry from '@sentry/node-experimental'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1.0, - tracePropagationTargets: [/\/v0/, 'v1'], - integrations: [Sentry.httpIntegration({ tracing: true })], -}); - -Sentry.startSpan({ name: 'test_transaction' }, () => { - http.get('http://match-this-url.com/api/v0'); - http.get('http://match-this-url.com/api/v1'); - http.get('http://dont-match-this-url.com/api/v2'); - http.get('http://dont-match-this-url.com/api/v3'); -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargetsDisabled/test.ts b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargetsDisabled/test.ts deleted file mode 100644 index 6fa28a13c5e1..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargetsDisabled/test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import nock from 'nock'; - -import { TestEnv, runScenario } from '../../../utils'; - -test('httpIntegration should not instrument when tracing is enabled', async () => { - const match1 = nock('http://match-this-url.com') - .get('/api/v0') - .matchHeader('baggage', val => val === undefined) - .matchHeader('sentry-trace', val => val === undefined) - .reply(200); - - const match2 = nock('http://match-this-url.com') - .get('/api/v1') - .matchHeader('baggage', val => val === undefined) - .matchHeader('sentry-trace', val => val === undefined) - .reply(200); - - const match3 = nock('http://dont-match-this-url.com') - .get('/api/v2') - .matchHeader('baggage', val => val === undefined) - .matchHeader('sentry-trace', val => val === undefined) - .reply(200); - - const match4 = nock('http://dont-match-this-url.com') - .get('/api/v3') - .matchHeader('baggage', val => val === undefined) - .matchHeader('sentry-trace', val => val === undefined) - .reply(200); - - const env = await TestEnv.init(__dirname); - await runScenario(env.url); - - env.server.close(); - nock.cleanAll(); - - await new Promise(resolve => env.server.close(resolve)); - - expect(match1.isDone()).toBe(true); - expect(match2.isDone()).toBe(true); - expect(match3.isDone()).toBe(true); - expect(match4.isDone()).toBe(true); -}); diff --git a/dev-packages/node-integration-tests/utils/runner.ts b/dev-packages/node-integration-tests/utils/runner.ts index 5da33508bda0..a9f3a94d4e3d 100644 --- a/dev-packages/node-integration-tests/utils/runner.ts +++ b/dev-packages/node-integration-tests/utils/runner.ts @@ -273,7 +273,7 @@ export function createRunner(...paths: string[]) { }); if (ensureNoErrorOutput) { - child.stderr.on('data', (data: Buffer) => { + child.stderr?.on('data', (data: Buffer) => { const output = data.toString(); complete(new Error(`Expected no error output but got: '${output}'`)); }); @@ -319,7 +319,7 @@ export function createRunner(...paths: string[]) { } let buffer = Buffer.alloc(0); - child.stdout.on('data', (data: Buffer) => { + child.stdout?.on('data', (data: Buffer) => { // This is horribly memory inefficient but it's only for tests buffer = Buffer.concat([buffer, data]); diff --git a/package.json b/package.json index 2d627a68b721..5c1f37ca4c33 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "@types/chai": "^4.1.3", "@types/jest": "^27.4.1", "@types/jsdom": "^16.2.3", - "@types/node": "~10.17.0", + "@types/node": "^14.18.0", "@types/rimraf": "^3.0.2", "@types/sinon": "^7.0.11", "@vitest/coverage-c8": "^0.29.2", diff --git a/packages/aws-serverless/package.json b/packages/aws-serverless/package.json index d8fe3c56544f..8a1c250337b9 100644 --- a/packages/aws-serverless/package.json +++ b/packages/aws-serverless/package.json @@ -44,7 +44,7 @@ "@types/express": "^4.17.14" }, "devDependencies": { - "@types/node": "^14.6.4", + "@types/node": "^14.18.0", "aws-sdk": "^2.765.0", "find-up": "^5.0.0", "nock": "^13.0.4", diff --git a/packages/browser/test/unit/profiling/utils.test.ts b/packages/browser/test/unit/profiling/utils.test.ts index a3141dfcb327..42a747f1aaa9 100644 --- a/packages/browser/test/unit/profiling/utils.test.ts +++ b/packages/browser/test/unit/profiling/utils.test.ts @@ -1,5 +1,4 @@ import { TextDecoder, TextEncoder } from 'util'; -// @ts-expect-error patch the encoder on the window, else importing JSDOM fails (deleted in afterAll) const patchedEncoder = (!global.window.TextEncoder && (global.window.TextEncoder = TextEncoder)) || true; // @ts-expect-error patch the encoder on the window, else importing JSDOM fails (deleted in afterAll) const patchedDecoder = (!global.window.TextDecoder && (global.window.TextDecoder = TextDecoder)) || true; @@ -19,31 +18,23 @@ const makeJSProfile = (partial: Partial = {}): JSSelfProfile => { }; }; -// @ts-expect-error store a reference so we can reset it later const globalDocument = global.document; -// @ts-expect-error store a reference so we can reset it later const globalWindow = global.window; -// @ts-expect-error store a reference so we can reset it later const globalLocation = global.location; describe('convertJSSelfProfileToSampledFormat', () => { beforeEach(() => { const dom = new JSDOM(); - // @ts-expect-error need to override global document global.document = dom.window.document; // @ts-expect-error need to override global document global.window = dom.window; - // @ts-expect-error need to override global document global.location = dom.window.location; }); // Reset back to previous values afterEach(() => { - // @ts-expect-error need to override global document global.document = globalDocument; - // @ts-expect-error need to override global document global.window = globalWindow; - // @ts-expect-error need to override global document global.location = globalLocation; }); diff --git a/packages/google-cloud-serverless/package.json b/packages/google-cloud-serverless/package.json index 02f6263b70a5..55be4e2b8753 100644 --- a/packages/google-cloud-serverless/package.json +++ b/packages/google-cloud-serverless/package.json @@ -48,7 +48,7 @@ "@google-cloud/common": "^3.4.1", "@google-cloud/functions-framework": "^1.7.1", "@google-cloud/pubsub": "^2.5.0", - "@types/node": "^14.6.4", + "@types/node": "^14.18.0", "find-up": "^5.0.0", "google-gax": "^2.9.0", "nock": "^13.0.4", diff --git a/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts index cca8e09fa3b8..81b2f88e7888 100644 --- a/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts @@ -116,7 +116,7 @@ export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameteriz if ( process.env.NODE_ENV === 'development' && !process.env.SENTRY_IGNORE_API_RESOLUTION_ERROR && - !res.finished + !res.writableEnded ) { consoleSandbox(() => { // eslint-disable-next-line no-console diff --git a/packages/nextjs/test/config/withSentry.test.ts b/packages/nextjs/test/config/withSentry.test.ts index 8f4c718f9612..6de52dbc1989 100644 --- a/packages/nextjs/test/config/withSentry.test.ts +++ b/packages/nextjs/test/config/withSentry.test.ts @@ -27,7 +27,10 @@ describe('withSentry', () => { this.end(); }, end: function (this: AugmentedNextApiResponse) { + // eslint-disable-next-line deprecation/deprecation this.finished = true; + // @ts-expect-error This is a mock + this.writableEnded = true; }, } as unknown as AugmentedNextApiResponse; }); diff --git a/packages/nextjs/test/edge/edgeWrapperUtils.test.ts b/packages/nextjs/test/edge/edgeWrapperUtils.test.ts index 1dea31811bbf..029ee9d97fce 100644 --- a/packages/nextjs/test/edge/edgeWrapperUtils.test.ts +++ b/packages/nextjs/test/edge/edgeWrapperUtils.test.ts @@ -3,9 +3,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { withEdgeWrapping } from '../../src/common/utils/edgeWrapperUtils'; -// @ts-expect-error Request does not exist on type Global const origRequest = global.Request; -// @ts-expect-error Response does not exist on type Global const origResponse = global.Response; // @ts-expect-error Request does not exist on type Global @@ -21,9 +19,7 @@ global.Request = class Request { global.Response = class Request {}; afterAll(() => { - // @ts-expect-error Request does not exist on type Global global.Request = origRequest; - // @ts-expect-error Response does not exist on type Global global.Response = origResponse; }); diff --git a/packages/nextjs/test/edge/withSentryAPI.test.ts b/packages/nextjs/test/edge/withSentryAPI.test.ts index f3b5bfded4a9..2b232c922968 100644 --- a/packages/nextjs/test/edge/withSentryAPI.test.ts +++ b/packages/nextjs/test/edge/withSentryAPI.test.ts @@ -3,9 +3,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } fr import { wrapApiHandlerWithSentry } from '../../src/edge'; -// @ts-expect-error Request does not exist on type Global const origRequest = global.Request; -// @ts-expect-error Response does not exist on type Global const origResponse = global.Response; // @ts-expect-error Request does not exist on type Global @@ -29,9 +27,7 @@ global.Request = class Request { global.Response = class Response {}; afterAll(() => { - // @ts-expect-error Request does not exist on type Global global.Request = origRequest; - // @ts-expect-error Response does not exist on type Global global.Response = origResponse; }); diff --git a/packages/node-experimental/package.json b/packages/node-experimental/package.json index 9618874e117a..042ca4dd1460 100644 --- a/packages/node-experimental/package.json +++ b/packages/node-experimental/package.json @@ -68,10 +68,10 @@ "@sentry/utils": "8.0.0-alpha.5" }, "devDependencies": { - "@types/node": "14.18.63" + "@types/node": "^14.18.0" }, "optionalDependencies": { - "opentelemetry-instrumentation-fetch-node": "1.1.0" + "opentelemetry-instrumentation-fetch-node": "1.1.2" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/node-experimental/test/integration/transactions.test.ts b/packages/node-experimental/test/integration/transactions.test.ts index 4dbe7c84da59..e9d45294d1e3 100644 --- a/packages/node-experimental/test/integration/transactions.test.ts +++ b/packages/node-experimental/test/integration/transactions.test.ts @@ -644,6 +644,8 @@ describe('Integration | Transactions', () => { await new Promise(resolve => setTimeout(resolve, 10 * 60 * 1000)); }); + jest.advanceTimersByTime(1); + // Child-spans have been added to the exporter, but they are pending since they are waiting for their parant expect(exporter['_finishedSpans'].length).toBe(2); expect(beforeSendTransaction).toHaveBeenCalledTimes(0); diff --git a/packages/opentelemetry/src/propagator.ts b/packages/opentelemetry/src/propagator.ts index 39be6c4d764c..b8de206d84f1 100644 --- a/packages/opentelemetry/src/propagator.ts +++ b/packages/opentelemetry/src/propagator.ts @@ -2,15 +2,20 @@ import type { Baggage, Context, SpanContext, TextMapGetter, TextMapSetter } from import { context } from '@opentelemetry/api'; import { TraceFlags, propagation, trace } from '@opentelemetry/api'; import { TraceState, W3CBaggagePropagator, isTracingSuppressed } from '@opentelemetry/core'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; import type { continueTrace } from '@sentry/core'; +import { spanToJSON } from '@sentry/core'; import { getClient, getCurrentScope, getDynamicSamplingContextFromClient, getIsolationScope } from '@sentry/core'; -import type { DynamicSamplingContext, PropagationContext } from '@sentry/types'; +import type { DynamicSamplingContext, Options, PropagationContext } from '@sentry/types'; import { + LRUMap, SENTRY_BAGGAGE_KEY_PREFIX, baggageHeaderToDynamicSamplingContext, dynamicSamplingContextToSentryBaggageHeader, generateSentryTraceHeader, + logger, propagationContextFromHeaders, + stringMatchesSomePattern, } from '@sentry/utils'; import { @@ -20,6 +25,7 @@ import { SENTRY_TRACE_STATE_PARENT_SPAN_ID, SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING, } from './constants'; +import { DEBUG_BUILD } from './debug-build'; import { getScopesFromContext, setScopesOnContext } from './utils/contextData'; import { setIsSetup } from './utils/setupCheck'; @@ -46,9 +52,15 @@ export function getPropagationContextFromSpanContext(spanContext: SpanContext): * Injects and extracts `sentry-trace` and `baggage` headers from carriers. */ export class SentryPropagator extends W3CBaggagePropagator { + /** A map of URLs that have already been checked for if they match tracePropagationTargets. */ + private _urlMatchesTargetsMap: LRUMap; + public constructor() { super(); setIsSetup('SentryPropagator'); + + // We're caching results so we don't have to recompute regexp every time we create a request. + this._urlMatchesTargetsMap = new LRUMap(100); } /** @@ -56,6 +68,20 @@ export class SentryPropagator extends W3CBaggagePropagator { */ public inject(context: Context, carrier: unknown, setter: TextMapSetter): void { if (isTracingSuppressed(context)) { + DEBUG_BUILD && logger.log('[Tracing] Not injecting trace data for url because tracing is suppressed.'); + return; + } + + const activeSpan = trace.getSpan(context); + const url = activeSpan && spanToJSON(activeSpan).data?.[SemanticAttributes.HTTP_URL]; + const tracePropagationTargets = getClient()?.getOptions()?.tracePropagationTargets; + if ( + typeof url === 'string' && + tracePropagationTargets && + !this._shouldInjectTraceData(tracePropagationTargets, url) + ) { + DEBUG_BUILD && + logger.log('[Tracing] Not injecting trace data for url because it does not matchTracePropagationTargets:', url); return; } @@ -112,6 +138,22 @@ export class SentryPropagator extends W3CBaggagePropagator { public fields(): string[] { return [SENTRY_TRACE_HEADER, SENTRY_BAGGAGE_HEADER]; } + + /** If we want to inject trace data for a given URL. */ + private _shouldInjectTraceData(tracePropagationTargets: Options['tracePropagationTargets'], url: string): boolean { + if (tracePropagationTargets === undefined) { + return true; + } + + const cachedDecision = this._urlMatchesTargetsMap.get(url); + if (cachedDecision !== undefined) { + return cachedDecision; + } + + const decision = stringMatchesSomePattern(url, tracePropagationTargets); + this._urlMatchesTargetsMap.set(url, decision); + return decision; + } } /** Exported for tests. */ diff --git a/packages/remix/src/utils/serverAdapters/express.ts b/packages/remix/src/utils/serverAdapters/express.ts index 7bf02700aedc..4a280efe1b4d 100644 --- a/packages/remix/src/utils/serverAdapters/express.ts +++ b/packages/remix/src/utils/serverAdapters/express.ts @@ -184,7 +184,7 @@ function wrapEndMethod(origEnd: ResponseEndMethod): WrappedResponseEndMethod { await finishSentryProcessing(this); return origEnd.call(this, ...args); - }; + } as unknown as WrappedResponseEndMethod; } /** diff --git a/packages/tracing-internal/test/browser/backgroundtab.test.ts b/packages/tracing-internal/test/browser/backgroundtab.test.ts index d44f08621ddc..e25eaa168fd4 100644 --- a/packages/tracing-internal/test/browser/backgroundtab.test.ts +++ b/packages/tracing-internal/test/browser/backgroundtab.test.ts @@ -9,7 +9,6 @@ describe('registerBackgroundTabDetection', () => { let events: Record = {}; beforeEach(() => { const dom = new JSDOM(); - // @ts-expect-error need to override global document global.document = dom.window.document; const options = getDefaultClientOptions({ tracesSampleRate: 1 }); @@ -19,7 +18,6 @@ describe('registerBackgroundTabDetection', () => { addTracingExtensions(); - // @ts-expect-error need to override global document global.document.addEventListener = jest.fn((event, callback) => { events[event] = callback; }); diff --git a/packages/utils/test/browser.test.ts b/packages/utils/test/browser.test.ts index 5c7df188664e..8b92f28a8536 100644 --- a/packages/utils/test/browser.test.ts +++ b/packages/utils/test/browser.test.ts @@ -4,9 +4,7 @@ import { getDomElement, htmlTreeAsString } from '../src/browser'; beforeAll(() => { const dom = new JSDOM(); - // @ts-expect-error need to override global document global.document = dom.window.document; - // @ts-expect-error need to add HTMLElement type or it will not be found global.HTMLElement = new JSDOM().window.HTMLElement; }); diff --git a/packages/vercel-edge/test/transports/index.test.ts b/packages/vercel-edge/test/transports/index.test.ts index 74f5d1794ff8..252522171780 100644 --- a/packages/vercel-edge/test/transports/index.test.ts +++ b/packages/vercel-edge/test/transports/index.test.ts @@ -25,13 +25,10 @@ class Headers { const mockFetch = jest.fn(); -// @ts-expect-error fetch is not on global const oldFetch = global.fetch; -// @ts-expect-error fetch is not on global global.fetch = mockFetch; afterAll(() => { - // @ts-expect-error fetch is not on global global.fetch = oldFetch; }); diff --git a/yarn.lock b/yarn.lock index c7da93a6f132..c3680ee0130b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6632,17 +6632,8 @@ dependencies: "@types/unist" "*" -"@types/history-4@npm:@types/history@4.7.8": - version "4.7.8" - resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" - integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== - -"@types/history-5@npm:@types/history@4.7.8": - version "4.7.8" - resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" - integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== - -"@types/history@*": +"@types/history-4@npm:@types/history@4.7.8", "@types/history-5@npm:@types/history@4.7.8", "@types/history@*": + name "@types/history-4" version "4.7.8" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== @@ -6898,7 +6889,7 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.38.tgz#f8bb07c371ccb1903f3752872c89f44006132947" integrity sha512-5jY9RhV7c0Z4Jy09G+NIDTsCZ5G0L5n+Z+p+Y7t5VJHM30bgwzSjVtlcBxqAj+6L/swIlvtOSzr8rBk/aNyV2g== -"@types/node@14.18.63": +"@types/node@14.18.63", "@types/node@^14.18.0": version "14.18.63" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.63.tgz#1788fa8da838dbb5f9ea994b834278205db6ca2b" integrity sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ== @@ -6913,16 +6904,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.2.tgz#d76fb80d87d0d8abfe334fc6d292e83e5524efc4" integrity sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w== -"@types/node@^10.1.0", "@types/node@~10.17.0": +"@types/node@^10.1.0": version "10.17.60" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b" integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== -"@types/node@^14.6.4": - version "14.14.37" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.37.tgz#a3dd8da4eb84a996c36e331df98d82abd76b516e" - integrity sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw== - "@types/node@^18.11.17": version "18.14.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.14.2.tgz#c076ed1d7b6095078ad3cf21dfeea951842778b1" @@ -7032,15 +7018,7 @@ "@types/history" "^3" "@types/react" "*" -"@types/react-router-4@npm:@types/react-router@5.1.14": - version "5.1.14" - resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.14.tgz#e0442f4eb4c446541ad7435d44a97f8fe6df40da" - integrity sha512-LAJpqYUaCTMT2anZheoidiIymt8MuX286zoVFPM3DVb23aQBH0mAkFvzpd4LKqiolV8bBtZWT5Qp7hClCNDENw== - dependencies: - "@types/history" "*" - "@types/react" "*" - -"@types/react-router-5@npm:@types/react-router@5.1.14": +"@types/react-router-4@npm:@types/react-router@5.1.14", "@types/react-router-5@npm:@types/react-router@5.1.14": version "5.1.14" resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.14.tgz#e0442f4eb4c446541ad7435d44a97f8fe6df40da" integrity sha512-LAJpqYUaCTMT2anZheoidiIymt8MuX286zoVFPM3DVb23aQBH0mAkFvzpd4LKqiolV8bBtZWT5Qp7hClCNDENw== @@ -23111,10 +23089,10 @@ open@^8.0.9: is-docker "^2.1.1" is-wsl "^2.2.0" -opentelemetry-instrumentation-fetch-node@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/opentelemetry-instrumentation-fetch-node/-/opentelemetry-instrumentation-fetch-node-1.1.0.tgz#f51d79862390f3a694fa91c35c4383e037a04c11" - integrity sha512-mSEpyRfwv6t1L+VvqTw5rCzNr3bVTsGE4/dcZruhFWivXFKl8pqm6W0LWPxHrEvwufw1eK9VmUgalfY0jjMl8Q== +opentelemetry-instrumentation-fetch-node@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/opentelemetry-instrumentation-fetch-node/-/opentelemetry-instrumentation-fetch-node-1.1.2.tgz#ba18648b8e1273c5e801a1d9d7a5e4c6f1daf6df" + integrity sha512-w5KYAw/X/F0smj1v67VQUhnWusS6+0y/v7W/H5iY6s1Zf7uOCCm0fiffY8t2H+4wXui2AOjcfEM77S6AvSTAJg== dependencies: "@opentelemetry/api" "^1.6.0" "@opentelemetry/instrumentation" "^0.43.0" @@ -25208,7 +25186,8 @@ react-is@^18.0.0: dependencies: "@remix-run/router" "1.0.2" -"react-router-6@npm:react-router@6.3.0": +"react-router-6@npm:react-router@6.3.0", react-router@6.3.0: + name react-router-6 version "6.3.0" resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== @@ -25223,13 +25202,6 @@ react-router-dom@^6.2.2: history "^5.2.0" react-router "6.3.0" -react-router@6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" - integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== - dependencies: - history "^5.2.0" - react@^18.0.0: version "18.0.0" resolved "https://registry.yarnpkg.com/react/-/react-18.0.0.tgz#b468736d1f4a5891f38585ba8e8fb29f91c3cb96" @@ -27709,7 +27681,7 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -27737,13 +27709,6 @@ strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" @@ -30405,16 +30370,7 @@ workerpool@^6.4.0: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.4.0.tgz#f8d5cfb45fde32fa3b7af72ad617c3369567a462" integrity sha512-i3KR1mQMNwY2wx20ozq2EjISGtQWDIfV56We+yGJ5yDs8jTwQiLLaqHlkBHITlCuJnYlVRmXegxFxZg7gqI++A== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@7.0.0, wrap-ansi@^5.1.0, wrap-ansi@^6.2.0, wrap-ansi@^7.0.0, wrap-ansi@^8.1.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@7.0.0, wrap-ansi@^5.1.0, wrap-ansi@^6.2.0, wrap-ansi@^7.0.0, wrap-ansi@^8.1.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==