diff --git a/.craft.yml b/.craft.yml index 27ff96e235d9..1e3c27f319a2 100644 --- a/.craft.yml +++ b/.craft.yml @@ -16,10 +16,10 @@ targets: - name: npm id: '@sentry/core' includeNames: /^sentry-core-\d.*\.tgz$/ - ## 1.4 Tracing package + ## 1.4 Browser Utils package - name: npm - id: '@sentry-internal/tracing' - includeNames: /^sentry-internal-tracing-\d.*\.tgz$/ + id: '@sentry-internal/browser-utils' + includeNames: /^sentry-internal-browser-utils-\d.*\.tgz$/ ## 1.5 Replay Internal package (browser only) - name: npm id: '@sentry-internal/replay' diff --git a/.eslintrc.js b/.eslintrc.js index 90f474319c7d..a9fb5f421af5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -51,7 +51,7 @@ module.exports = { }, }, { - files: ['scenarios/**', 'dev-packages/rollup-utils/**'], + files: ['scenarios/**', 'dev-packages/rollup-utils/**', 'dev-packages/bundle-analyzer-scenarios/**'], parserOptions: { sourceType: 'module', }, @@ -59,5 +59,11 @@ module.exports = { 'no-console': 'off', }, }, + { + files: ['vite.config.ts'], + parserOptions: { + project: ['tsconfig.test.json'], + }, + }, ], }; diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index c9bf669eb28c..f3589c97781e 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -13,5 +13,11 @@ ef6b3c7877d5fc8031c08bb28b0ffafaeb01f501 # chore: Enforce formatting of MD files in repository root #10127 aecf26f22dbf65ce2c0caadc4ce71b46266c9f45 -# chore: Create dev-packages folder #9997 +# chore: Create dev-packages folder #9997 35205b4cc5783237e69452c39ea001e461d9c84d + +# ref: Move node & node-experimental folders #11309 +# As well as revert and re-revert of this +971b51d4b8e92aa1b93c51074e28c7cbed63b486 +ebc9b539548953bb9dd81d6a18adcdd91e804563 +c88ff463a5566194a454b58bc555f183cf9ee813 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7a0d64822d13..b131d8cb2717 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -96,13 +96,12 @@ jobs: - 'scripts/**' - 'packages/core/**' - 'packages/rollup-utils/**' - - 'packages/tracing/**' - - 'packages/tracing-internal/**' - 'packages/utils/**' - 'packages/types/**' browser: &browser - *shared - 'packages/browser/**' + - 'packages/browser-utils/**' - 'packages/replay/**' - 'packages/replay-canvas/**' - 'packages/feedback/**' @@ -118,7 +117,6 @@ jobs: node: - *shared - 'packages/node/**' - - 'packages/node-experimental/**' - 'dev-packages/node-integration-tests/**' nextjs: - *shared @@ -135,7 +133,6 @@ jobs: profiling_node: - *shared - 'packages/node/**' - - 'packages/node-experimental/**' - 'packages/profiling-node/**' - 'dev-packages/e2e-tests/test-applications/node-profiling/**' profiling_node_bindings: @@ -1039,6 +1036,7 @@ jobs: 'create-remix-app-v2', 'create-remix-app-express-vite-dev', 'debug-id-sourcemaps', + # 'esm-loader-node-express-app', # This is currently broken for upstream reasons. See https://github.com/getsentry/sentry-javascript/pull/11338#issuecomment-2025450675 'nextjs-app-dir', 'nextjs-14', 'react-create-hash-router', @@ -1050,8 +1048,11 @@ jobs: 'node-fastify-app', # TODO(v8): Re-enable hapi tests # 'node-hapi-app', + 'node-nestjs-app', 'node-exports-test-app', + 'node-koa-app', 'vue-3', + 'webpack-4', 'webpack-5' ] build-command: diff --git a/.github/workflows/flaky-test-detector.yml b/.github/workflows/flaky-test-detector.yml index 4643827dcebf..78496c34b476 100644 --- a/.github/workflows/flaky-test-detector.yml +++ b/.github/workflows/flaky-test-detector.yml @@ -3,7 +3,7 @@ on: workflow_dispatch: pull_request: paths: - - 'dev-packages/browser-integration-tests/suites/**' + - 'dev-packages/browser-integration-tests/suites/**/test.ts' branches-ignore: - master diff --git a/.vscode/settings.json b/.vscode/settings.json index 2950621966b9..1a8f9ce92cfc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -38,5 +38,8 @@ "editor.codeActionsOnSave": { "source.organizeImports.biome": "explicit", }, - "editor.defaultFormatter": "biomejs.biome" + "editor.defaultFormatter": "biomejs.biome", + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 60fc8cfbb58f..e52d23393fa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,91 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 8.0.0-alpha.8 + +This is the eighth alpha release of Sentry JavaScript SDK v8, which includes a variety of breaking changes. + +Read the [in-depth migration guide](./MIGRATION.md) to find out how to address any breaking changes in your code. + +### Important Changes + +- **feat: Add @sentry-internal/browser-utils (#11381)** + +A big part of the browser-runtime specific exports of the internal `@sentry/utils` package were moved into a new package +`@sentry-internal/browser-utils`. If you imported any API from `@sentry/utils` (which is generally not recommended but +necessary for some workarounds), please check that your import statements still point to existing exports after +upgrading. + +- **feat: Add loader file to node-based SDKs to support ESM monkeypatching (#11338)** + +When using ESM, it is necessary to use a "loader" to be able to instrument certain third-party packages and Node.js API. +The server-side SDKs now ship with a set of ESM loader hooks, that should be used when using ESM. Use them as follows: + +```sh +# For Node.js <= 18.18.2 +node --experimental-loader=@sentry/node/hook your-app.js + +# For Node.js >= 18.19.0 +node --import=@sentry/node/register your-app.js +``` + +Please note that due to an upstream bug, these loader hooks will currently crash or simply not work. We are planning to +fix this in upcoming versions of the SDK - definitely before a stable version 8 release. + +- **feat(node): Add Koa error handler (#11403)** +- **feat(node): Add NestJS error handler (#11375)** + +The Sentry SDK now exports integrations and error middlewares for Koa (`koaIntegration()`, `setupKoaErrorHandler()`) and +NestJS (`setupNestErrorHandler()`) that can be used to instrument your Koa and NestJS applications with error +monitoring. + +### Removal/Refactoring of deprecated functionality + +- feat(core): Remove hub check in isSentryRequestUrl (#11407) +- feat(opentelemetry): Remove top level startActiveSpan (#11380) +- feat(types): `beforeSend` and `beforeSendTransaction` breaking changes (#11354) +- feat(v8): Remove all class based integrations (#11345) +- feat(v8/core): Remove span.attributes top level field (#11378) +- ref: Remove convertIntegrationFnToClass (#11343) +- ref(node): Remove the old `node` package (#11322) +- ref(tracing): Remove `span.startChild()` (#11376) +- ref(v8): Remove `addRequestDataToTransaction` util (#11369) +- ref(v8): Remove `args` on `HandlerDataXhr` (#11373) +- ref(v8): Remove `getGlobalObject` utility method (#11372) +- ref(v8): Remove `metadata` on transaction (#11397) +- ref(v8): Remove `pushScope`, `popScope`, `isOlderThan`, `shouldSendDefaultPii` from hub (#11404) +- ref(v8): Remove `shouldCreateSpanForRequest` from vercel edge options (#11371) +- ref(v8): Remove deprecated `_reportAllChanges` option (#11393) +- ref(v8): Remove deprecated `scope.getTransaction()` (#11365) +- ref(v8): Remove deprecated methods on scope (#11366) +- ref(v8): Remove deprecated span & transaction properties (#11400) +- ref(v8): Remove Transaction concept (#11422) + +### Other Changes + +- feat: Add `trpcMiddleware` back to serverside SDKs (#11374) +- feat: Implement timed events & remove `transaction.measurements` (#11398) +- feat(browser): Bump web-vitals to 3.5.2 (#11391) +- feat(feedback): Add `getFeedback` utility to get typed feedback instance (#11331) +- feat(otel): Do not sample `options` and `head` requests (#11467) +- feat(remix): Update scope `transactionName` when resolving route (#11420) +- feat(replay): Bump `rrweb` to 2.12.0 (#11314) +- feat(replay): Use data sentry element as fallback for the component name (#11383) +- feat(sveltekit): Update scope `transactionName` when pageload route name is updated (#11406) +- feat(tracing-internal): Reset propagation context on navigation (#11401) +- feat(types): Add View Hierarchy types (#11409) +- feat(utils): Use `globalThis` (#11351) +- feat(vue): Update scope's `transactionName` when resolving a route (#11423) +- fix(core): unref timer to not block node exit (#11430) +- fix(node): Fix baggage propagation (#11363) +- fix(web-vitals): Check for undefined navigation entry (#11311) +- ref: Set preserveModules to true for browser packages (#11452) +- ref(core): Remove duplicate logic in scope.update (#11384) +- ref(feedback): Add font family style to actor (#11432) +- ref(feedback): Add font family to buttons (#11414) +- ref(gcp-serverless): Remove setting `.__sentry_transaction` (#11346) +- ref(nextjs): Replace multiplexer with conditional exports (#11442) + ## 8.0.0-alpha.7 This is the seventh alpha release of Sentry JavaScript SDK v8, which includes a variety of breaking changes. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d424a69e1967..a02b096ed125 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,9 @@ We welcome suggested improvements and bug fixes to the `@sentry/*` family of packages, in the form of pull requests on [`GitHub`](https://github.com/getsentry/sentry-javascript). The guide below will help you get started, but if you have -further questions, please feel free to reach out on [Discord](https://discord.gg/Ww9hbqr). +further questions, please feel free to reach out on [Discord](https://discord.gg/Ww9hbqr). To learn about some general +SDK development principles check out the [SDK Development Guide](https://develop.sentry.dev/sdk/) in the Sentry +Developer Documentation. ## Setting up an Environment diff --git a/MIGRATION.md b/MIGRATION.md index 5a8f01c28875..51bf19cddb01 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -43,6 +43,8 @@ For IE11 support please transpile your code to ES5 using babel or similar and ad **Next.js**: The Next.js SDK now supports Next.js 13.2.0+ +**Express**: Complex router setups are only properly parametrized in Node 16+. + ## 2. Package removal We've removed the following packages: @@ -569,7 +571,7 @@ Sentry.getGlobalScope().addEventProcessor(event => { The `lastEventId` function has been removed. See [below](./MIGRATION.md#deprecate-lasteventid) for more details. -#### Remove `void` from transport return types +#### Removal of `void` from transport return types The `send` method on the `Transport` interface now always requires a `TransportMakeRequestResponse` to be returned in the promise. This means that the `void` return type is no longer allowed. @@ -588,7 +590,7 @@ interface Transport { } ``` -#### Remove `addGlobalEventProcessor` in favor of `addEventProcessor` +#### Removal of `addGlobalEventProcessor` in favor of `addEventProcessor` In v8, we are removing the `addGlobalEventProcessor` function in favor of `addEventProcessor`. @@ -608,6 +610,23 @@ addEventProcessor(event => { }); ``` +#### Removal of `Sentry.Handlers.trpcMiddleware()` in favor of `Sentry.trpcMiddleware()` + +The Sentry tRPC middleware got moved from `Sentry.Handlers.trpcMiddleware()` to `Sentry.trpcMiddleware()`. Functionally +they are the same: + +```js +// v7 +import * as Sentry from '@sentry/node'; +Sentry.Handlers.trpcMiddleware(); +``` + +```js +// v8 +import * as Sentry from '@sentry/node'; +Sentry.trpcMiddleware(); +``` + ### Browser SDK (Browser, React, Vue, Angular, Ember, etc.) Removed top-level exports: `Offline`, `makeXHRTransport`, `BrowserTracing`, `wrap` diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/console/capture/subject.js b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/console/capture/subject.js new file mode 100644 index 000000000000..d9ee50bf556f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/console/capture/subject.js @@ -0,0 +1,10 @@ +console.log('One'); +console.warn('Two', { a: 1 }); +console.error('Error 2', { b: { c: [] } }); + +// Passed assertions _should not_ be captured +console.assert(1 + 1 === 2, 'math works'); +// Failed assertions _should_ be captured +console.assert(1 + 1 === 3, 'math broke'); + +Sentry.captureException('test exception'); diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/console/capture/test.ts b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/console/capture/test.ts new file mode 100644 index 000000000000..de53e2cee485 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/console/capture/test.ts @@ -0,0 +1,45 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/browser'; + +import { sentryTest } from '../../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers'; + +sentryTest('should capture console breadcrumbs', async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.breadcrumbs).toEqual([ + { + category: 'console', + data: { arguments: ['One'], logger: 'console' }, + level: 'log', + message: 'One', + timestamp: expect.any(Number), + }, + { + category: 'console', + data: { arguments: ['Two', { a: 1 }], logger: 'console' }, + level: 'warning', + message: 'Two [object Object]', + timestamp: expect.any(Number), + }, + { + category: 'console', + data: { arguments: ['Error 2', { b: '[Object]' }], logger: 'console' }, + level: 'error', + message: 'Error 2 [object Object]', + timestamp: expect.any(Number), + }, + { + category: 'console', + data: { + arguments: ['math broke'], + logger: 'console', + }, + level: 'log', + message: 'Assertion failed: math broke', + timestamp: expect.any(Number), + }, + ]); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/console/init.js b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/console/init.js new file mode 100644 index 000000000000..36806d01c6d0 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/console/init.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + defaultIntegrations: false, + integrations: [Sentry.breadcrumbsIntegration()], + sampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/template.html b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/template.html index e54da47ff09d..97d2b9069eb4 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/template.html +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/template.html @@ -7,6 +7,7 @@ - + + diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/test.ts b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/test.ts index 0b877fb7c8d8..d6dd646b9965 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/test.ts @@ -78,6 +78,7 @@ sentryTest( await page.goto(url); await page.locator('#annotated-button').click(); + await page.locator('#annotated-button-2').click(); const [eventData] = await Promise.all([promise, page.evaluate('Sentry.captureException("test exception")')]); @@ -88,6 +89,12 @@ sentryTest( message: 'body > AnnotatedButton', data: { 'ui.component_name': 'AnnotatedButton' }, }, + { + timestamp: expect.any(Number), + category: 'ui.click', + message: 'body > StyledButton', + data: { 'ui.component_name': 'StyledButton' }, + }, ]); }, ); diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/clickWithError/subject.js b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/clickWithError/subject.js new file mode 100644 index 000000000000..9a0c89788ea7 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/clickWithError/subject.js @@ -0,0 +1,7 @@ +const click = new MouseEvent('click'); +function kaboom() { + throw new Error('lol'); +} +Object.defineProperty(click, 'target', { get: kaboom }); +const input = document.getElementById('input1'); +input.dispatchEvent(click); diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/clickWithError/template.html b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/clickWithError/template.html new file mode 100644 index 000000000000..cba1da8d531d --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/clickWithError/template.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/clickWithError/test.ts b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/clickWithError/test.ts new file mode 100644 index 000000000000..d965a4ac0d7d --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/clickWithError/test.ts @@ -0,0 +1,32 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/browser'; + +import { sentryTest } from '../../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers'; + +// see: https://github.com/getsentry/sentry-javascript/issues/768 +sentryTest( + 'should record breadcrumb if accessing the target property of an event throws an exception', + async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.goto(url); + + const promise = getFirstSentryEnvelopeRequest(page); + + await page.locator('#input1').pressSequentially('test', { delay: 1 }); + + await page.evaluate('Sentry.captureException("test exception")'); + + const eventData = await promise; + + expect(eventData.breadcrumbs).toHaveLength(1); + expect(eventData.breadcrumbs).toEqual([ + { + category: 'ui.input', + message: 'body > input#input1[type="text"]', + timestamp: expect.any(Number), + }, + ]); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/customEvent/subject.js b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/customEvent/subject.js new file mode 100644 index 000000000000..ca08cace4134 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/customEvent/subject.js @@ -0,0 +1,9 @@ +const input = document.getElementsByTagName('input')[0]; +input.addEventListener('build', function (evt) { + evt.stopPropagation(); +}); + +const customEvent = new CustomEvent('build', { detail: 1 }); +input.dispatchEvent(customEvent); + +Sentry.captureException('test exception'); diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/customEvent/template.html b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/customEvent/template.html new file mode 100644 index 000000000000..a16ca41e45da --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/customEvent/template.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/customEvent/test.ts b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/customEvent/test.ts new file mode 100644 index 000000000000..83cd53f8acba --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/customEvent/test.ts @@ -0,0 +1,19 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers'; + +sentryTest('breadcrumbs listener should not fail with custom event', async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + + let error = undefined; + page.on('pageerror', err => { + error = err; + }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + expect(eventData.exception?.values).toHaveLength(1); + expect(eventData.breadcrumbs).toBeUndefined(); + expect(error).toBeUndefined(); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/multipleTypes/template.html b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/multipleTypes/template.html new file mode 100644 index 000000000000..cba1da8d531d --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/multipleTypes/template.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/multipleTypes/test.ts b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/multipleTypes/test.ts new file mode 100644 index 000000000000..53372fcacc97 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/multipleTypes/test.ts @@ -0,0 +1,50 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/browser'; + +import { sentryTest } from '../../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers'; + +sentryTest( + 'should correctly capture multiple consecutive breadcrumbs if they are of different type', + async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.goto(url); + + const promise = getFirstSentryEnvelopeRequest(page); + + // These inputs will be debounced + await page.locator('#input1').pressSequentially('abc', { delay: 1 }); + await page.locator('#input1').pressSequentially('def', { delay: 1 }); + await page.locator('#input1').pressSequentially('ghi', { delay: 1 }); + + await page.locator('#input1').click(); + await page.locator('#input1').click(); + await page.locator('#input1').click(); + + // This input should not be debounced + await page.locator('#input1').pressSequentially('jkl', { delay: 1 }); + + await page.evaluate('Sentry.captureException("test exception")'); + + const eventData = await promise; + + expect(eventData.breadcrumbs).toEqual([ + { + category: 'ui.input', + message: 'body > input#input1[type="text"]', + timestamp: expect.any(Number), + }, + { + category: 'ui.click', + message: 'body > input#input1[type="text"]', + timestamp: expect.any(Number), + }, + { + category: 'ui.input', + message: 'body > input#input1[type="text"]', + timestamp: expect.any(Number), + }, + ]); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/template.html b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/template.html index a16ca41e45da..38934ca803a4 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/template.html +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/template.html @@ -7,6 +7,7 @@ - + + diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/test.ts b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/test.ts index b4549a105c7a..d7544fbf891e 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/test.ts @@ -83,6 +83,7 @@ sentryTest( await page.goto(url); await page.locator('#annotated-input').pressSequentially('John', { delay: 1 }); + await page.locator('#annotated-input-2').pressSequentially('John', { delay: 1 }); await page.evaluate('Sentry.captureException("test exception")'); const eventData = await promise; @@ -95,6 +96,12 @@ sentryTest( message: 'body > AnnotatedInput', data: { 'ui.component_name': 'AnnotatedInput' }, }, + { + timestamp: expect.any(Number), + category: 'ui.input', + message: 'body > StyledInput', + data: { 'ui.component_name': 'StyledInput' }, + }, ]); }, ); diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/get/subject.js b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/get/subject.js index fc9ffd720768..f6e1e21e4611 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/get/subject.js +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/get/subject.js @@ -1,5 +1,3 @@ -const xhr = new XMLHttpRequest(); - fetch('http://localhost:7654/foo').then(() => { Sentry.captureException('test error'); }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/getWithRequestObj/subject.js b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/getWithRequestObj/subject.js new file mode 100644 index 000000000000..0ca20f1b5acb --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/getWithRequestObj/subject.js @@ -0,0 +1,3 @@ +fetch(new Request('http://localhost:7654/foo')).then(() => { + Sentry.captureException('test error'); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/getWithRequestObj/test.ts b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/getWithRequestObj/test.ts new file mode 100644 index 000000000000..3ffa68776fd2 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/getWithRequestObj/test.ts @@ -0,0 +1,37 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers'; + +sentryTest('captures Breadcrumb for basic GET request that uses request object', async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.route('**/foo', route => { + return route.fulfill({ + status: 200, + body: JSON.stringify({ + userNames: ['John', 'Jane'], + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.exception?.values).toHaveLength(1); + + expect(eventData?.breadcrumbs?.length).toBe(1); + expect(eventData!.breadcrumbs![0]).toEqual({ + timestamp: expect.any(Number), + category: 'fetch', + type: 'http', + data: { + method: 'GET', + status_code: 200, + url: 'http://localhost:7654/foo', + }, + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/post/subject.js b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/post/subject.js index 595e9395aa80..ea1bf44bc905 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/post/subject.js +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/post/subject.js @@ -1,5 +1,3 @@ -const xhr = new XMLHttpRequest(); - fetch('http://localhost:7654/foo', { method: 'POST', body: '{"my":"body"}', diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/history/init.js b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/history/init.js new file mode 100644 index 000000000000..36806d01c6d0 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/history/init.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + defaultIntegrations: false, + integrations: [Sentry.breadcrumbsIntegration()], + sampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/history/navigation/subject.js b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/history/navigation/subject.js new file mode 100644 index 000000000000..dd1d47ef4dff --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/history/navigation/subject.js @@ -0,0 +1,7 @@ +history.pushState({}, '', '/foo'); +history.pushState({}, '', '/bar?a=1#fragment'); +history.pushState({}, '', {}); +history.pushState({}, '', null); +history.replaceState({}, '', '/bar?a=1#fragment'); + +Sentry.captureException('test exception'); diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/history/navigation/test.ts b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/history/navigation/test.ts new file mode 100644 index 000000000000..91bd6faaf083 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/history/navigation/test.ts @@ -0,0 +1,46 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/browser'; + +import { sentryTest } from '../../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers'; + +sentryTest('should record history changes as navigation breadcrumbs', async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.breadcrumbs).toEqual([ + { + category: 'navigation', + data: { + from: '/index.html', + to: '/foo', + }, + timestamp: expect.any(Number), + }, + { + category: 'navigation', + data: { + from: '/foo', + to: '/bar?a=1#fragment', + }, + timestamp: expect.any(Number), + }, + { + category: 'navigation', + data: { + from: '/bar?a=1#fragment', + to: '[object Object]', + }, + timestamp: expect.any(Number), + }, + { + category: 'navigation', + data: { + from: '[object Object]', + to: '/bar?a=1#fragment', + }, + timestamp: expect.any(Number), + }, + ]); +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/linkedErrrors/subject.js b/dev-packages/browser-integration-tests/suites/public-api/captureException/linkedErrors/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/public-api/captureException/linkedErrrors/subject.js rename to dev-packages/browser-integration-tests/suites/public-api/captureException/linkedErrors/subject.js diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/linkedErrrors/test.ts b/dev-packages/browser-integration-tests/suites/public-api/captureException/linkedErrors/test.ts similarity index 100% rename from dev-packages/browser-integration-tests/suites/public-api/captureException/linkedErrrors/test.ts rename to dev-packages/browser-integration-tests/suites/public-api/captureException/linkedErrors/test.ts diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/multipleErrorsDifferentStacktrace/subject.js b/dev-packages/browser-integration-tests/suites/public-api/captureException/multipleErrorsDifferentStacktrace/subject.js new file mode 100644 index 000000000000..d24f30aa2824 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureException/multipleErrorsDifferentStacktrace/subject.js @@ -0,0 +1,41 @@ +function bar() { + baz(); +} + +function foo() { + bar(); +} + +function foo2() { + // identical to foo, but meant for testing + // different stack frame fns w/ same stack length + bar(); +} +// same error message, but different stacks means that these are considered +// different errors + +// stack: +// bar +try { + bar(); +} catch (e) { + Sentry.captureException(e); +} + +// stack (different # frames): +// bar +// foo +try { + foo(); +} catch (e) { + Sentry.captureException(e); +} + +// stack (same # frames, different frames): +// bar +// foo2 +try { + foo2(); +} catch (e) { + Sentry.captureException(e); +} diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/multipleErrorsDifferentStacktrace/test.ts b/dev-packages/browser-integration-tests/suites/public-api/captureException/multipleErrorsDifferentStacktrace/test.ts new file mode 100644 index 000000000000..277c518d58f0 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureException/multipleErrorsDifferentStacktrace/test.ts @@ -0,0 +1,19 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers'; + +sentryTest('should not reject back-to-back errors with different stack traces', async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + + const eventData = await getMultipleSentryEnvelopeRequests(page, 3, { url }); + + // NOTE: regex because exact error message differs per-browser + expect(eventData[0].exception?.values?.[0].value).toMatch(/baz/); + expect(eventData[0].exception?.values?.[0].type).toMatch('ReferenceError'); + expect(eventData[1].exception?.values?.[0].value).toMatch(/baz/); + expect(eventData[1].exception?.values?.[0].type).toMatch('ReferenceError'); + expect(eventData[2].exception?.values?.[0].value).toMatch(/baz/); + expect(eventData[2].exception?.values?.[0].type).toMatch('ReferenceError'); +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/multipleErrorsSameStacktrace/subject.js b/dev-packages/browser-integration-tests/suites/public-api/captureException/multipleErrorsSameStacktrace/subject.js new file mode 100644 index 000000000000..5feab6646ccc --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureException/multipleErrorsSameStacktrace/subject.js @@ -0,0 +1,55 @@ +function throwError(message) { + // eslint-disable-next-line no-param-reassign + message = message || 'foo'; + try { + throw new Error(message); + } catch (o_O) { + Sentry.captureException(o_O); + } +} + +function throwRandomError() { + try { + throw new Error('Exception no ' + (Date.now() + Math.random())); + } catch (o_O) { + Sentry.captureException(o_O); + } +} + +function createError() { + function nestedFunction() { + return new Error('created'); + } + + return nestedFunction(); +} + +function throwSameConsecutiveErrors(message) { + throwError(message); + throwError(message); +} + +// Different exceptions, don't dedupe +for (var i = 0; i < 2; i++) { + throwRandomError(); +} + +// Same exceptions and same stacktrace, dedupe +for (var j = 0; j < 2; j++) { + throwError(); +} + +const syntheticError = createError(); + +// Same exception, with transaction in between, dedupe +Sentry.captureException(syntheticError); +Sentry.captureEvent({ + event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', + message: 'someMessage', + transaction: 'wat', + type: 'transaction', +}); +Sentry.captureException(syntheticError); + +// Same exceptions, different stacktrace (different line number), don't dedupe +throwSameConsecutiveErrors('bar'); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/multipleErrorsSameStacktrace/test.ts b/dev-packages/browser-integration-tests/suites/public-api/captureException/multipleErrorsSameStacktrace/test.ts new file mode 100644 index 000000000000..acd7e12ed351 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureException/multipleErrorsSameStacktrace/test.ts @@ -0,0 +1,21 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers'; + +sentryTest('should reject duplicate, back-to-back errors from captureException', async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + + const eventData = await getMultipleSentryEnvelopeRequests(page, 7, { url }); + + // NOTE: regex because exact error message differs per-browser + expect(eventData[0].exception?.values?.[0].value).toMatch(/Exception no \d+/); + expect(eventData[1].exception?.values?.[0].value).toMatch(/Exception no \d+/); + expect(eventData[2].exception?.values?.[0].value).toEqual('foo'); + // transaction so undefined + expect(eventData[3].exception?.values?.[0].value).toEqual('created'); + expect(eventData[4].exception?.values?.[0].value).toEqual(undefined); + expect(eventData[5].exception?.values?.[0].value).toEqual('bar'); + expect(eventData[6].exception?.values?.[0].value).toEqual('bar'); +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureMessage/multipleMessageAttachStacktrace/init.js b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/multipleMessageAttachStacktrace/init.js new file mode 100644 index 000000000000..99ffd9f0ab31 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/multipleMessageAttachStacktrace/init.js @@ -0,0 +1,8 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + attachStacktrace: true, +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureMessage/multipleMessageAttachStacktrace/subject.js b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/multipleMessageAttachStacktrace/subject.js new file mode 100644 index 000000000000..f95ddde16833 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/multipleMessageAttachStacktrace/subject.js @@ -0,0 +1,27 @@ +function captureMessage(message) { + // eslint-disable-next-line no-param-reassign + message = message || 'message'; + Sentry.captureMessage(message); +} + +function captureRandomMessage() { + Sentry.captureMessage('Message no ' + (Date.now() + Math.random())); +} + +function captureSameConsecutiveMessages(message) { + captureMessage(message); + captureMessage(message); +} + +// Different messages, don't dedupe +for (var i = 0; i < 2; i++) { + captureRandomMessage(); +} + +// Same messages and same stacktrace, dedupe +for (var j = 0; j < 3; j++) { + captureMessage('same message, same stacktrace'); +} + +// Same messages, different stacktrace (different line number), don't dedupe +captureSameConsecutiveMessages('same message, different stacktrace'); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureMessage/multipleMessageAttachStacktrace/test.ts b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/multipleMessageAttachStacktrace/test.ts new file mode 100644 index 000000000000..01845d983feb --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/multipleMessageAttachStacktrace/test.ts @@ -0,0 +1,20 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers'; + +sentryTest( + 'should reject duplicate, back-to-back messages from captureMessage when it has stacktrace', + async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + + const eventData = await getMultipleSentryEnvelopeRequests(page, 5, { url }); + + expect(eventData[0].message).toMatch(/Message no \d+/); + expect(eventData[1].message).toMatch(/Message no \d+/); + expect(eventData[2].message).toMatch('same message, same stacktrace'); + expect(eventData[3].message).toMatch('same message, different stacktrace'); + expect(eventData[4].message).toMatch('same message, different stacktrace'); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/public-api/init/stringSampleRate/init.js b/dev-packages/browser-integration-tests/suites/public-api/init/stringSampleRate/init.js new file mode 100644 index 000000000000..bebdec192df0 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/init/stringSampleRate/init.js @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +window._errorCount = 0; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: '0', + beforeSend() { + window._errorCount++; + return null; + }, +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/init/stringSampleRate/subject.js b/dev-packages/browser-integration-tests/suites/public-api/init/stringSampleRate/subject.js new file mode 100644 index 000000000000..12de659fad4e --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/init/stringSampleRate/subject.js @@ -0,0 +1,3 @@ +Sentry.captureException(new Error('test error')); + +window._testDone = true; diff --git a/dev-packages/browser-integration-tests/suites/public-api/init/stringSampleRate/test.ts b/dev-packages/browser-integration-tests/suites/public-api/init/stringSampleRate/test.ts new file mode 100644 index 000000000000..b5aeb4a3182f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/init/stringSampleRate/test.ts @@ -0,0 +1,16 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../utils/fixtures'; + +sentryTest('parses a string sample rate', async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.goto(url); + + await page.waitForFunction('window._testDone'); + await page.evaluate('window.Sentry.getClient().flush()'); + + const count = await page.evaluate('window._errorCount'); + + expect(count).toStrictEqual(0); +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/circular_data/subject.js b/dev-packages/browser-integration-tests/suites/public-api/startSpan/circular_data/subject.js index baa6bd3d2361..b5ee8624b124 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/circular_data/subject.js +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/circular_data/subject.js @@ -2,6 +2,6 @@ const chicken = {}; const egg = { contains: chicken }; chicken.lays = egg; -Sentry.startSpan({ name: 'circular_object_test_transaction', data: { chicken } }, () => { - Sentry.startSpan({ op: 'circular_object_test_span', data: { chicken } }, () => undefined); +Sentry.startSpan({ name: 'circular_object_test_transaction', attributes: { chicken } }, () => { + Sentry.startSpan({ op: 'circular_object_test_span', attributes: { chicken } }, () => undefined); }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/setMeasurement/subject.js b/dev-packages/browser-integration-tests/suites/public-api/startSpan/setMeasurement/subject.js index 9316ed946b2d..61c9de0fa617 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/setMeasurement/subject.js +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/setMeasurement/subject.js @@ -1,11 +1,6 @@ -const transaction = Sentry.startInactiveSpan({ - name: 'some_transaction', - forceTransaction: true, +Sentry.startSpan({ name: 'some_transaction' }, () => { + Sentry.setMeasurement('metric.foo', 42, 'ms'); + Sentry.setMeasurement('metric.bar', 1337, 'nanoseconds'); + Sentry.setMeasurement('metric.baz', 99, 's'); + Sentry.setMeasurement('metric.baz', 1, ''); }); - -transaction.setMeasurement('metric.foo', 42, 'ms'); -transaction.setMeasurement('metric.bar', 1337, 'nanoseconds'); -transaction.setMeasurement('metric.baz', 99, 's'); -transaction.setMeasurement('metric.baz', 1); - -transaction.end(); diff --git a/dev-packages/browser-integration-tests/suites/replay/captureComponentName/template.html b/dev-packages/browser-integration-tests/suites/replay/captureComponentName/template.html index 1cb45daa349a..2501d595be00 100644 --- a/dev-packages/browser-integration-tests/suites/replay/captureComponentName/template.html +++ b/dev-packages/browser-integration-tests/suites/replay/captureComponentName/template.html @@ -4,7 +4,9 @@ - - + + + + diff --git a/dev-packages/browser-integration-tests/suites/replay/captureComponentName/test.ts b/dev-packages/browser-integration-tests/suites/replay/captureComponentName/test.ts index 99b7a71273e3..9ad1a99c32aa 100644 --- a/dev-packages/browser-integration-tests/suites/replay/captureComponentName/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/captureComponentName/test.ts @@ -57,7 +57,10 @@ sentryTest('captures component name attribute when available', async ({ forceFlu data: { nodeId: expect.any(Number), node: { - attributes: { id: 'button', 'data-sentry-component': 'MyCoolButton' }, + attributes: { + id: 'button', + 'data-sentry-component': 'MyCoolButton', + }, id: expect.any(Number), tagName: 'button', textContent: '**', @@ -72,7 +75,95 @@ sentryTest('captures component name attribute when available', async ({ forceFlu data: { nodeId: expect.any(Number), node: { - attributes: { id: 'input', 'data-sentry-component': 'MyCoolInput' }, + attributes: { + id: 'input', + 'data-sentry-component': 'MyCoolInput', + }, + id: expect.any(Number), + tagName: 'input', + textContent: '', + }, + }, + }, + ]); +}); + +sentryTest('sets element name to component name attribute', async ({ forceFlushReplay, getLocalTestPath, page }) => { + if (shouldSkipReplayTest()) { + sentryTest.skip(); + } + + const reqPromise0 = waitForReplayRequest(page, 0); + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.goto(url); + await reqPromise0; + await forceFlushReplay(); + + const reqPromise1 = waitForReplayRequest(page, (event, res) => { + return getCustomRecordingEvents(res).breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.click'); + }); + const reqPromise2 = waitForReplayRequest(page, (event, res) => { + return getCustomRecordingEvents(res).breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.input'); + }); + + await page.locator('#button2').click(); + + await page.locator('#input2').focus(); + await page.keyboard.press('Control+A'); + await page.keyboard.type('Hello', { delay: 10 }); + + await forceFlushReplay(); + const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1); + const { breadcrumbs: breadcrumbs2 } = getCustomRecordingEvents(await reqPromise2); + + // Combine the two together + breadcrumbs2.forEach(breadcrumb => { + if (!breadcrumbs.some(b => b.category === breadcrumb.category && b.timestamp === breadcrumb.timestamp)) { + breadcrumbs.push(breadcrumb); + } + }); + + expect(breadcrumbs).toEqual([ + { + timestamp: expect.any(Number), + type: 'default', + category: 'ui.click', + message: 'body > StyledCoolButton', + data: { + nodeId: expect.any(Number), + node: { + attributes: { + id: 'button2', + 'data-sentry-component': 'StyledCoolButton', + }, + id: expect.any(Number), + tagName: 'button', + textContent: '**', + }, + }, + }, + { + timestamp: expect.any(Number), + type: 'default', + category: 'ui.input', + message: 'body > StyledCoolInput', + data: { + nodeId: expect.any(Number), + node: { + attributes: { + id: 'input2', + 'data-sentry-component': 'StyledCoolInput', + }, id: expect.any(Number), tagName: 'input', textContent: '', diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/assets/script.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/assets/script.js index a37a2c70ad27..fecb5ca8a758 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/assets/script.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/assets/script.js @@ -15,3 +15,4 @@ const delay = e => { document.querySelector('[data-test-id=interaction-button]').addEventListener('click', delay); document.querySelector('[data-test-id=annotated-button]').addEventListener('click', delay); +document.querySelector('[data-test-id=styled-button]').addEventListener('click', delay); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/template.html index 3357fb20a94e..d3bda54d7443 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/template.html +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/template.html @@ -6,7 +6,8 @@
Rendered Before Long Task
- + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts index a66d447d92ec..5b032ace7f8c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts @@ -1,6 +1,6 @@ import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import type { Event, SpanContext, SpanJSON } from '@sentry/types'; +import type { Contexts, Event, SpanJSON } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { @@ -11,7 +11,7 @@ import { type TransactionJSON = SpanJSON & { spans: SpanJSON[]; - contexts: SpanContext; + contexts: Contexts; platform: string; type: string; }; @@ -112,3 +112,35 @@ sentryTest( expect(interactionSpan.description).toBe('body > AnnotatedButton'); }, ); + +sentryTest( + 'should use the element name for a clicked element when no component name', + async ({ browserName, getLocalTestPath, page }) => { + const supportedBrowsers = ['chromium', 'firefox']; + + if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) { + sentryTest.skip(); + } + + await page.route('**/path/to/script.js', (route: Route) => + route.fulfill({ path: `${__dirname}/assets/script.js` }), + ); + + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.goto(url); + await getFirstSentryEnvelopeRequest(page); + + await page.locator('[data-test-id=styled-button]').click(); + + const envelopes = await getMultipleSentryEnvelopeRequests(page, 1); + expect(envelopes).toHaveLength(1); + const eventData = envelopes[0]; + + expect(eventData.spans).toHaveLength(1); + + const interactionSpan = eventData.spans![0]; + expect(interactionSpan.op).toBe('ui.interaction.click'); + expect(interactionSpan.description).toBe('body > StyledButton'); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts index 5a46a65a4392..f7dc01ca7f54 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts @@ -49,3 +49,25 @@ sentryTest('should create a navigation transaction on page navigation', async ({ expect(pageloadSpanId).not.toEqual(navigationSpanId); }); + +sentryTest('should create a new trace for for multiple navigations', async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + await getFirstSentryEnvelopeRequest(page, url); + const navigationEvent1 = await getFirstSentryEnvelopeRequest(page, `${url}#foo`); + const navigationEvent2 = await getFirstSentryEnvelopeRequest(page, `${url}#bar`); + + expect(navigationEvent1.contexts?.trace?.op).toBe('navigation'); + expect(navigationEvent2.contexts?.trace?.op).toBe('navigation'); + + const navigation1TraceId = navigationEvent1.contexts?.trace?.trace_id; + const navigation2TraceId = navigationEvent2.contexts?.trace?.trace_id; + + expect(navigation1TraceId).toBeDefined(); + expect(navigation2TraceId).toBeDefined(); + expect(navigation1TraceId).not.toEqual(navigation2TraceId); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/subject.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/subject.js index d0f8df871ee3..784df44a0c17 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/subject.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/subject.js @@ -1,4 +1,4 @@ -import { addLcpInstrumentationHandler } from '@sentry-internal/tracing'; +import { addLcpInstrumentationHandler } from '@sentry-internal/browser-utils'; addLcpInstrumentationHandler(({ metric }) => { const entry = metric.entries[metric.entries.length - 1]; diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb/test.ts index 0a4b1e6d3da6..e135601f7ddf 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb/test.ts @@ -4,15 +4,22 @@ import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; -sentryTest('should capture TTFB vital.', async ({ getLocalTestPath, page }) => { +sentryTest('should capture TTFB vital.', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); } - const url = await getLocalTestPath({ testDir: __dirname }); + const url = await getLocalTestUrl({ testDir: __dirname }); const eventData = await getFirstSentryEnvelopeRequest(page, url); expect(eventData.measurements).toBeDefined(); - expect(eventData.measurements?.ttfb?.value).toBeDefined(); + + // If responseStart === 0, ttfb is not reported + // This seems to happen somewhat randomly, so we just ignore this in that case + const responseStart = await page.evaluate("performance.getEntriesByType('navigation')[0].responseStart;"); + if (responseStart !== 0) { + expect(eventData.measurements?.ttfb?.value).toBeDefined(); + } + expect(eventData.measurements?.['ttfb.requestTime']?.value).toBeDefined(); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/stringSampleRate/init.js b/dev-packages/browser-integration-tests/suites/tracing/stringSampleRate/init.js new file mode 100644 index 000000000000..dbefdbdfcda5 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/stringSampleRate/init.js @@ -0,0 +1,8 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: '1', +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/stringSampleRate/subject.js b/dev-packages/browser-integration-tests/suites/tracing/stringSampleRate/subject.js new file mode 100644 index 000000000000..a14b9cd597ac --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/stringSampleRate/subject.js @@ -0,0 +1,3 @@ +Sentry.startSpan({ name: 'test span' }, () => { + // noop +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/stringSampleRate/test.ts b/dev-packages/browser-integration-tests/suites/tracing/stringSampleRate/test.ts new file mode 100644 index 000000000000..1b411d16e85b --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/stringSampleRate/test.ts @@ -0,0 +1,17 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequestOnUrl } from '../../../utils/helpers'; + +sentryTest('parses a string sample rate', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const req = await waitForTransactionRequestOnUrl(page, url); + const eventData = envelopeRequestParser(req); + + expect(eventData.contexts?.trace?.data?.['sentry.sample_rate']).toStrictEqual(1); +}); diff --git a/dev-packages/bundle-analyzer-scenarios/README.md b/dev-packages/bundle-analyzer-scenarios/README.md new file mode 100644 index 000000000000..97bd3033d1bb --- /dev/null +++ b/dev-packages/bundle-analyzer-scenarios/README.md @@ -0,0 +1,15 @@ +# Bundle Analyzer Scenarios + +This repository contains a set of scenarios to check the SDK against webpack bundle analyzer. + +You can run the scenarios by running `yarn analyze` and selecting the scenario you want to run. + +If you want to have more granular analysis of modules, you can build the SDK packages with with `preserveModules` set to +`true`. You can do this via the `SENTRY_BUILD_PRESERVE_MODULES`. + +```bash +SENTRY_BUILD_PRESERVE_MODULES=true yarn build +``` + +Please note that `preserveModules` has different behaviour with regards to tree-shaking, so you will get different total +bundle size results. diff --git a/dev-packages/bundle-analyzer-scenarios/browser-basic/index.js b/dev-packages/bundle-analyzer-scenarios/browser-basic/index.js new file mode 100644 index 000000000000..f3d47c97f7a2 --- /dev/null +++ b/dev-packages/bundle-analyzer-scenarios/browser-basic/index.js @@ -0,0 +1,5 @@ +import { init } from '@sentry/browser'; + +init({ + dsn: 'https://00000000000000000000000000000000@o000000.ingest.sentry.io/0000000', +}); diff --git a/dev-packages/bundle-analyzer-scenarios/browser-basic/package.json b/dev-packages/bundle-analyzer-scenarios/browser-basic/package.json new file mode 100644 index 000000000000..07aec65d5a4f --- /dev/null +++ b/dev-packages/bundle-analyzer-scenarios/browser-basic/package.json @@ -0,0 +1,4 @@ +{ + "type": "module", + "main": "index.js" +} diff --git a/dev-packages/bundle-analyzer-scenarios/package.json b/dev-packages/bundle-analyzer-scenarios/package.json new file mode 100644 index 000000000000..c07b013af7e6 --- /dev/null +++ b/dev-packages/bundle-analyzer-scenarios/package.json @@ -0,0 +1,23 @@ +{ + "name": "@sentry-internal/bundle-analyzer-scenarios", + "version": "8.0.0-alpha.7", + "description": "Scenarios to test bundle analysis with", + "repository": "git://github.com/getsentry/sentry-javascript.git", + "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/dev-packages/bundle-analyzer-scenarios", + "author": "Sentry", + "license": "MIT", + "private": true, + "dependencies": { + "html-webpack-plugin": "^5.5.0", + "webpack": "^5.76.0", + "webpack-bundle-analyzer": "^4.5.0", + "inquirer": "^8.2.0" + }, + "scripts": { + "analyze": "node webpack.cjs" + }, + "volta": { + "extends": "../../package.json" + }, + "type": "module" +} diff --git a/dev-packages/bundle-analyzer-scenarios/webpack.cjs b/dev-packages/bundle-analyzer-scenarios/webpack.cjs new file mode 100644 index 000000000000..aac95e59348a --- /dev/null +++ b/dev-packages/bundle-analyzer-scenarios/webpack.cjs @@ -0,0 +1,83 @@ +const path = require('path'); +const { promises } = require('fs'); + +const inquirer = require('inquirer'); +const webpack = require('webpack'); +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; +const HtmlWebpackPlugin = require('html-webpack-plugin'); + +async function init() { + const scenarios = await getScenariosFromDirectories(); + + const answers = await inquirer.prompt([ + { + type: 'rawlist', + name: 'scenario', + message: 'Which scenario you want to run?', + choices: scenarios, + pageSize: scenarios.length, + loop: false, + }, + ]); + + console.log(`Bundling scenario: ${answers.scenario}`); + + await runWebpack(answers.scenario); +} + +async function runWebpack(scenario) { + const alias = await generateAlias(); + + webpack( + { + mode: 'production', + entry: path.resolve(__dirname, scenario), + output: { + filename: 'main.js', + path: path.resolve(__dirname, 'dist', scenario), + }, + plugins: [new BundleAnalyzerPlugin({ analyzerMode: 'static' }), new HtmlWebpackPlugin()], + resolve: { + alias, + }, + }, + (err, stats) => { + if (err || stats.hasErrors()) { + console.log(err); + } + + // console.log('DONE', stats); + }, + ); +} + +const PACKAGE_PATH = '../../packages'; + +/** + * Generate webpack aliases based on packages in monorepo + * Example of an alias: '@sentry/serverless': 'path/to/sentry-javascript/packages/serverless', + */ +async function generateAlias() { + const dirents = await promises.readdir(PACKAGE_PATH); + + return Object.fromEntries( + await Promise.all( + dirents.map(async d => { + const packageJSON = JSON.parse(await promises.readFile(path.resolve(PACKAGE_PATH, d, 'package.json'))); + return [packageJSON['name'], path.resolve(PACKAGE_PATH, d)]; + }), + ), + ); +} + +/** + * Generates an array of available scenarios + */ +async function getScenariosFromDirectories() { + const exclude = ['node_modules', 'dist', '~', 'package.json', 'yarn.lock', 'README.md', '.DS_Store', 'webpack.cjs']; + + const dirents = await promises.readdir(path.join(__dirname), { withFileTypes: true }); + return dirents.map(dirent => dirent.name).filter(mape => !exclude.includes(mape)); +} + +init(); diff --git a/dev-packages/e2e-tests/test-applications/angular-17/package.json b/dev-packages/e2e-tests/test-applications/angular-17/package.json index e5609c250659..dee9c2921abe 100644 --- a/dev-packages/e2e-tests/test-applications/angular-17/package.json +++ b/dev-packages/e2e-tests/test-applications/angular-17/package.json @@ -29,6 +29,7 @@ "zone.js": "~0.14.3" }, "devDependencies": { + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", "@angular-devkit/build-angular": "^17.1.1", "@angular/cli": "^17.1.1", "@angular/compiler-cli": "^17.1.0", diff --git a/dev-packages/e2e-tests/test-applications/angular-17/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/angular-17/start-event-proxy.ts index 56fe43416adc..7bf4c4417f3c 100644 --- a/dev-packages/e2e-tests/test-applications/angular-17/start-event-proxy.ts +++ b/dev-packages/e2e-tests/test-applications/angular-17/start-event-proxy.ts @@ -1,4 +1,4 @@ -import { startEventProxyServer } from './event-proxy-server'; +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; startEventProxyServer({ port: 3031, diff --git a/dev-packages/e2e-tests/test-applications/angular-17/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/angular-17/tests/errors.test.ts index 4666893b1882..28e07284b435 100644 --- a/dev-packages/e2e-tests/test-applications/angular-17/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/angular-17/tests/errors.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '../event-proxy-server'; +import { waitForError, waitForTransaction } from '@sentry-internal/event-proxy-server'; test('sends an error', async ({ page }) => { const errorPromise = waitForError('angular-17', async errorEvent => { diff --git a/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts index 9fc4b74e779f..7873af286315 100644 --- a/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; -import { waitForTransaction } from '../event-proxy-server'; test('sends a pageload transaction with a parameterized URL', async ({ page }) => { const transactionPromise = waitForTransaction('angular-17', async transactionEvent => { diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/event-proxy-server.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/event-proxy-server.ts deleted file mode 100644 index d14ca5cb5e72..000000000000 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/event-proxy-server.ts +++ /dev/null @@ -1,253 +0,0 @@ -import * as fs from 'fs'; -import * as http from 'http'; -import * as https from 'https'; -import type { AddressInfo } from 'net'; -import * as os from 'os'; -import * as path from 'path'; -import * as util from 'util'; -import * as zlib from 'zlib'; -import type { Envelope, EnvelopeItem, Event } from '@sentry/types'; -import { parseEnvelope } from '@sentry/utils'; - -const readFile = util.promisify(fs.readFile); -const writeFile = util.promisify(fs.writeFile); - -interface EventProxyServerOptions { - /** Port to start the event proxy server at. */ - port: number; - /** The name for the proxy server used for referencing it with listener functions */ - proxyServerName: string; -} - -interface SentryRequestCallbackData { - envelope: Envelope; - rawProxyRequestBody: string; - rawSentryResponseBody: string; - sentryResponseStatusCode?: number; -} - -/** - * Starts an event proxy server that will proxy events to sentry when the `tunnel` option is used. Point the `tunnel` - * option to this server (like this `tunnel: http://localhost:${port option}/`). - */ -export async function startEventProxyServer(options: EventProxyServerOptions): Promise { - const eventCallbackListeners: Set<(data: string) => void> = new Set(); - - const proxyServer = http.createServer((proxyRequest, proxyResponse) => { - const proxyRequestChunks: Uint8Array[] = []; - - proxyRequest.addListener('data', (chunk: Buffer) => { - proxyRequestChunks.push(chunk); - }); - - proxyRequest.addListener('error', err => { - throw err; - }); - - proxyRequest.addListener('end', () => { - const proxyRequestBody = - proxyRequest.headers['content-encoding'] === 'gzip' - ? zlib.gunzipSync(Buffer.concat(proxyRequestChunks)).toString() - : Buffer.concat(proxyRequestChunks).toString(); - - let envelopeHeader = JSON.parse(proxyRequestBody.split('\n')[0]); - - if (!envelopeHeader.dsn) { - throw new Error('[event-proxy-server] No dsn on envelope header. Please set tunnel option.'); - } - - const { origin, pathname, host } = new URL(envelopeHeader.dsn); - - const projectId = pathname.substring(1); - const sentryIngestUrl = `${origin}/api/${projectId}/envelope/`; - - proxyRequest.headers.host = host; - - const sentryResponseChunks: Uint8Array[] = []; - - const sentryRequest = https.request( - sentryIngestUrl, - { headers: proxyRequest.headers, method: proxyRequest.method }, - sentryResponse => { - sentryResponse.addListener('data', (chunk: Buffer) => { - proxyResponse.write(chunk, 'binary'); - sentryResponseChunks.push(chunk); - }); - - sentryResponse.addListener('end', () => { - eventCallbackListeners.forEach(listener => { - const rawSentryResponseBody = Buffer.concat(sentryResponseChunks).toString(); - - const data: SentryRequestCallbackData = { - envelope: parseEnvelope(proxyRequestBody), - rawProxyRequestBody: proxyRequestBody, - rawSentryResponseBody, - sentryResponseStatusCode: sentryResponse.statusCode, - }; - - listener(Buffer.from(JSON.stringify(data)).toString('base64')); - }); - proxyResponse.end(); - }); - - sentryResponse.addListener('error', err => { - throw err; - }); - - proxyResponse.writeHead(sentryResponse.statusCode || 500, sentryResponse.headers); - }, - ); - - sentryRequest.write(Buffer.concat(proxyRequestChunks), 'binary'); - sentryRequest.end(); - }); - }); - - const proxyServerStartupPromise = new Promise(resolve => { - proxyServer.listen(options.port, () => { - resolve(); - }); - }); - - const eventCallbackServer = http.createServer((eventCallbackRequest, eventCallbackResponse) => { - eventCallbackResponse.statusCode = 200; - eventCallbackResponse.setHeader('connection', 'keep-alive'); - - const callbackListener = (data: string): void => { - eventCallbackResponse.write(data.concat('\n'), 'utf8'); - }; - - eventCallbackListeners.add(callbackListener); - - eventCallbackRequest.on('close', () => { - eventCallbackListeners.delete(callbackListener); - }); - - eventCallbackRequest.on('error', () => { - eventCallbackListeners.delete(callbackListener); - }); - }); - - const eventCallbackServerStartupPromise = new Promise(resolve => { - eventCallbackServer.listen(0, () => { - const port = String((eventCallbackServer.address() as AddressInfo).port); - void registerCallbackServerPort(options.proxyServerName, port).then(resolve); - }); - }); - - await eventCallbackServerStartupPromise; - await proxyServerStartupPromise; - return; -} - -export async function waitForRequest( - proxyServerName: string, - callback: (eventData: SentryRequestCallbackData) => Promise | boolean, -): Promise { - const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName); - - return new Promise((resolve, reject) => { - const request = http.request(`http://localhost:${eventCallbackServerPort}/`, {}, response => { - let eventContents = ''; - - response.on('error', err => { - reject(err); - }); - - response.on('data', (chunk: Buffer) => { - const chunkString = chunk.toString('utf8'); - chunkString.split('').forEach(char => { - if (char === '\n') { - const eventCallbackData: SentryRequestCallbackData = JSON.parse( - Buffer.from(eventContents, 'base64').toString('utf8'), - ); - const callbackResult = callback(eventCallbackData); - if (typeof callbackResult !== 'boolean') { - callbackResult.then( - match => { - if (match) { - response.destroy(); - resolve(eventCallbackData); - } - }, - err => { - throw err; - }, - ); - } else if (callbackResult) { - response.destroy(); - resolve(eventCallbackData); - } - eventContents = ''; - } else { - eventContents = eventContents.concat(char); - } - }); - }); - }); - - request.end(); - }); -} - -export function waitForEnvelopeItem( - proxyServerName: string, - callback: (envelopeItem: EnvelopeItem) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForRequest(proxyServerName, async eventData => { - const envelopeItems = eventData.envelope[1]; - for (const envelopeItem of envelopeItems) { - if (await callback(envelopeItem)) { - resolve(envelopeItem); - return true; - } - } - return false; - }).catch(reject); - }); -} - -export function waitForError( - proxyServerName: string, - callback: (transactionEvent: Event) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForEnvelopeItem(proxyServerName, async envelopeItem => { - const [envelopeItemHeader, envelopeItemBody] = envelopeItem; - if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) { - resolve(envelopeItemBody as Event); - return true; - } - return false; - }).catch(reject); - }); -} - -export function waitForTransaction( - proxyServerName: string, - callback: (transactionEvent: Event) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForEnvelopeItem(proxyServerName, async envelopeItem => { - const [envelopeItemHeader, envelopeItemBody] = envelopeItem; - if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) { - resolve(envelopeItemBody as Event); - return true; - } - return false; - }).catch(reject); - }); -} - -const TEMP_FILE_PREFIX = 'event-proxy-server-'; - -async function registerCallbackServerPort(serverName: string, port: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - await writeFile(tmpFilePath, port, { encoding: 'utf8' }); -} - -function retrieveCallbackServerPort(serverName: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - return readFile(tmpFilePath, 'utf8'); -} diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json index 4310039b0638..98b587aa96cb 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json @@ -25,6 +25,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", "@playwright/test": "^1.36.2", "@remix-run/dev": "^2.7.2", "@sentry/types": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/start-event-proxy.ts index e56a52190e63..8ba8517f5f8a 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/start-event-proxy.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/start-event-proxy.ts @@ -1,4 +1,4 @@ -import { startEventProxyServer } from './event-proxy-server'; +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; startEventProxyServer({ port: 3031, proxyServerName: 'create-remix-app-express-vite-dev', diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/behaviour-server.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/behaviour-server.test.ts index 09cee3ca79bc..428dcb6d8668 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/behaviour-server.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/behaviour-server.test.ts @@ -1,7 +1,7 @@ import { expect, test } from '@playwright/test'; import { uuid4 } from '@sentry/utils'; -import { waitForTransaction } from '../event-proxy-server'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; test('Sends two linked transactions (server & client) to Sentry', async ({ page }) => { // We use this to identify the transactions diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/event-proxy-server.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/event-proxy-server.ts deleted file mode 100644 index d14ca5cb5e72..000000000000 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/event-proxy-server.ts +++ /dev/null @@ -1,253 +0,0 @@ -import * as fs from 'fs'; -import * as http from 'http'; -import * as https from 'https'; -import type { AddressInfo } from 'net'; -import * as os from 'os'; -import * as path from 'path'; -import * as util from 'util'; -import * as zlib from 'zlib'; -import type { Envelope, EnvelopeItem, Event } from '@sentry/types'; -import { parseEnvelope } from '@sentry/utils'; - -const readFile = util.promisify(fs.readFile); -const writeFile = util.promisify(fs.writeFile); - -interface EventProxyServerOptions { - /** Port to start the event proxy server at. */ - port: number; - /** The name for the proxy server used for referencing it with listener functions */ - proxyServerName: string; -} - -interface SentryRequestCallbackData { - envelope: Envelope; - rawProxyRequestBody: string; - rawSentryResponseBody: string; - sentryResponseStatusCode?: number; -} - -/** - * Starts an event proxy server that will proxy events to sentry when the `tunnel` option is used. Point the `tunnel` - * option to this server (like this `tunnel: http://localhost:${port option}/`). - */ -export async function startEventProxyServer(options: EventProxyServerOptions): Promise { - const eventCallbackListeners: Set<(data: string) => void> = new Set(); - - const proxyServer = http.createServer((proxyRequest, proxyResponse) => { - const proxyRequestChunks: Uint8Array[] = []; - - proxyRequest.addListener('data', (chunk: Buffer) => { - proxyRequestChunks.push(chunk); - }); - - proxyRequest.addListener('error', err => { - throw err; - }); - - proxyRequest.addListener('end', () => { - const proxyRequestBody = - proxyRequest.headers['content-encoding'] === 'gzip' - ? zlib.gunzipSync(Buffer.concat(proxyRequestChunks)).toString() - : Buffer.concat(proxyRequestChunks).toString(); - - let envelopeHeader = JSON.parse(proxyRequestBody.split('\n')[0]); - - if (!envelopeHeader.dsn) { - throw new Error('[event-proxy-server] No dsn on envelope header. Please set tunnel option.'); - } - - const { origin, pathname, host } = new URL(envelopeHeader.dsn); - - const projectId = pathname.substring(1); - const sentryIngestUrl = `${origin}/api/${projectId}/envelope/`; - - proxyRequest.headers.host = host; - - const sentryResponseChunks: Uint8Array[] = []; - - const sentryRequest = https.request( - sentryIngestUrl, - { headers: proxyRequest.headers, method: proxyRequest.method }, - sentryResponse => { - sentryResponse.addListener('data', (chunk: Buffer) => { - proxyResponse.write(chunk, 'binary'); - sentryResponseChunks.push(chunk); - }); - - sentryResponse.addListener('end', () => { - eventCallbackListeners.forEach(listener => { - const rawSentryResponseBody = Buffer.concat(sentryResponseChunks).toString(); - - const data: SentryRequestCallbackData = { - envelope: parseEnvelope(proxyRequestBody), - rawProxyRequestBody: proxyRequestBody, - rawSentryResponseBody, - sentryResponseStatusCode: sentryResponse.statusCode, - }; - - listener(Buffer.from(JSON.stringify(data)).toString('base64')); - }); - proxyResponse.end(); - }); - - sentryResponse.addListener('error', err => { - throw err; - }); - - proxyResponse.writeHead(sentryResponse.statusCode || 500, sentryResponse.headers); - }, - ); - - sentryRequest.write(Buffer.concat(proxyRequestChunks), 'binary'); - sentryRequest.end(); - }); - }); - - const proxyServerStartupPromise = new Promise(resolve => { - proxyServer.listen(options.port, () => { - resolve(); - }); - }); - - const eventCallbackServer = http.createServer((eventCallbackRequest, eventCallbackResponse) => { - eventCallbackResponse.statusCode = 200; - eventCallbackResponse.setHeader('connection', 'keep-alive'); - - const callbackListener = (data: string): void => { - eventCallbackResponse.write(data.concat('\n'), 'utf8'); - }; - - eventCallbackListeners.add(callbackListener); - - eventCallbackRequest.on('close', () => { - eventCallbackListeners.delete(callbackListener); - }); - - eventCallbackRequest.on('error', () => { - eventCallbackListeners.delete(callbackListener); - }); - }); - - const eventCallbackServerStartupPromise = new Promise(resolve => { - eventCallbackServer.listen(0, () => { - const port = String((eventCallbackServer.address() as AddressInfo).port); - void registerCallbackServerPort(options.proxyServerName, port).then(resolve); - }); - }); - - await eventCallbackServerStartupPromise; - await proxyServerStartupPromise; - return; -} - -export async function waitForRequest( - proxyServerName: string, - callback: (eventData: SentryRequestCallbackData) => Promise | boolean, -): Promise { - const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName); - - return new Promise((resolve, reject) => { - const request = http.request(`http://localhost:${eventCallbackServerPort}/`, {}, response => { - let eventContents = ''; - - response.on('error', err => { - reject(err); - }); - - response.on('data', (chunk: Buffer) => { - const chunkString = chunk.toString('utf8'); - chunkString.split('').forEach(char => { - if (char === '\n') { - const eventCallbackData: SentryRequestCallbackData = JSON.parse( - Buffer.from(eventContents, 'base64').toString('utf8'), - ); - const callbackResult = callback(eventCallbackData); - if (typeof callbackResult !== 'boolean') { - callbackResult.then( - match => { - if (match) { - response.destroy(); - resolve(eventCallbackData); - } - }, - err => { - throw err; - }, - ); - } else if (callbackResult) { - response.destroy(); - resolve(eventCallbackData); - } - eventContents = ''; - } else { - eventContents = eventContents.concat(char); - } - }); - }); - }); - - request.end(); - }); -} - -export function waitForEnvelopeItem( - proxyServerName: string, - callback: (envelopeItem: EnvelopeItem) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForRequest(proxyServerName, async eventData => { - const envelopeItems = eventData.envelope[1]; - for (const envelopeItem of envelopeItems) { - if (await callback(envelopeItem)) { - resolve(envelopeItem); - return true; - } - } - return false; - }).catch(reject); - }); -} - -export function waitForError( - proxyServerName: string, - callback: (transactionEvent: Event) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForEnvelopeItem(proxyServerName, async envelopeItem => { - const [envelopeItemHeader, envelopeItemBody] = envelopeItem; - if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) { - resolve(envelopeItemBody as Event); - return true; - } - return false; - }).catch(reject); - }); -} - -export function waitForTransaction( - proxyServerName: string, - callback: (transactionEvent: Event) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForEnvelopeItem(proxyServerName, async envelopeItem => { - const [envelopeItemHeader, envelopeItemBody] = envelopeItem; - if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) { - resolve(envelopeItemBody as Event); - return true; - } - return false; - }).catch(reject); - }); -} - -const TEMP_FILE_PREFIX = 'event-proxy-server-'; - -async function registerCallbackServerPort(serverName: string, port: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - await writeFile(tmpFilePath, port, { encoding: 'utf8' }); -} - -function retrieveCallbackServerPort(serverName: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - return readFile(tmpFilePath, 'utf8'); -} diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json index 646bc5f21e25..25f13aa5fd3f 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json @@ -21,6 +21,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", "@playwright/test": "^1.36.2", "@remix-run/dev": "2.7.2", "@remix-run/eslint-config": "2.7.2", diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/start-event-proxy.ts index cc810192de58..069eee5c7f34 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/start-event-proxy.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/start-event-proxy.ts @@ -1,4 +1,4 @@ -import { startEventProxyServer } from './event-proxy-server'; +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; startEventProxyServer({ port: 3031, proxyServerName: 'create-remix-app-v2', diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/behaviour-server.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/behaviour-server.test.ts index 992a315af3d3..42a5344c5e79 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/behaviour-server.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/behaviour-server.test.ts @@ -1,7 +1,7 @@ import { expect, test } from '@playwright/test'; import { uuid4 } from '@sentry/utils'; -import { waitForTransaction } from '../event-proxy-server'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; test('Sends two linked transactions (server & client) to Sentry', async ({ page }) => { // We use this to identify the transactions diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app/event-proxy-server.ts b/dev-packages/e2e-tests/test-applications/create-remix-app/event-proxy-server.ts deleted file mode 100644 index d14ca5cb5e72..000000000000 --- a/dev-packages/e2e-tests/test-applications/create-remix-app/event-proxy-server.ts +++ /dev/null @@ -1,253 +0,0 @@ -import * as fs from 'fs'; -import * as http from 'http'; -import * as https from 'https'; -import type { AddressInfo } from 'net'; -import * as os from 'os'; -import * as path from 'path'; -import * as util from 'util'; -import * as zlib from 'zlib'; -import type { Envelope, EnvelopeItem, Event } from '@sentry/types'; -import { parseEnvelope } from '@sentry/utils'; - -const readFile = util.promisify(fs.readFile); -const writeFile = util.promisify(fs.writeFile); - -interface EventProxyServerOptions { - /** Port to start the event proxy server at. */ - port: number; - /** The name for the proxy server used for referencing it with listener functions */ - proxyServerName: string; -} - -interface SentryRequestCallbackData { - envelope: Envelope; - rawProxyRequestBody: string; - rawSentryResponseBody: string; - sentryResponseStatusCode?: number; -} - -/** - * Starts an event proxy server that will proxy events to sentry when the `tunnel` option is used. Point the `tunnel` - * option to this server (like this `tunnel: http://localhost:${port option}/`). - */ -export async function startEventProxyServer(options: EventProxyServerOptions): Promise { - const eventCallbackListeners: Set<(data: string) => void> = new Set(); - - const proxyServer = http.createServer((proxyRequest, proxyResponse) => { - const proxyRequestChunks: Uint8Array[] = []; - - proxyRequest.addListener('data', (chunk: Buffer) => { - proxyRequestChunks.push(chunk); - }); - - proxyRequest.addListener('error', err => { - throw err; - }); - - proxyRequest.addListener('end', () => { - const proxyRequestBody = - proxyRequest.headers['content-encoding'] === 'gzip' - ? zlib.gunzipSync(Buffer.concat(proxyRequestChunks)).toString() - : Buffer.concat(proxyRequestChunks).toString(); - - let envelopeHeader = JSON.parse(proxyRequestBody.split('\n')[0]); - - if (!envelopeHeader.dsn) { - throw new Error('[event-proxy-server] No dsn on envelope header. Please set tunnel option.'); - } - - const { origin, pathname, host } = new URL(envelopeHeader.dsn); - - const projectId = pathname.substring(1); - const sentryIngestUrl = `${origin}/api/${projectId}/envelope/`; - - proxyRequest.headers.host = host; - - const sentryResponseChunks: Uint8Array[] = []; - - const sentryRequest = https.request( - sentryIngestUrl, - { headers: proxyRequest.headers, method: proxyRequest.method }, - sentryResponse => { - sentryResponse.addListener('data', (chunk: Buffer) => { - proxyResponse.write(chunk, 'binary'); - sentryResponseChunks.push(chunk); - }); - - sentryResponse.addListener('end', () => { - eventCallbackListeners.forEach(listener => { - const rawSentryResponseBody = Buffer.concat(sentryResponseChunks).toString(); - - const data: SentryRequestCallbackData = { - envelope: parseEnvelope(proxyRequestBody), - rawProxyRequestBody: proxyRequestBody, - rawSentryResponseBody, - sentryResponseStatusCode: sentryResponse.statusCode, - }; - - listener(Buffer.from(JSON.stringify(data)).toString('base64')); - }); - proxyResponse.end(); - }); - - sentryResponse.addListener('error', err => { - throw err; - }); - - proxyResponse.writeHead(sentryResponse.statusCode || 500, sentryResponse.headers); - }, - ); - - sentryRequest.write(Buffer.concat(proxyRequestChunks), 'binary'); - sentryRequest.end(); - }); - }); - - const proxyServerStartupPromise = new Promise(resolve => { - proxyServer.listen(options.port, () => { - resolve(); - }); - }); - - const eventCallbackServer = http.createServer((eventCallbackRequest, eventCallbackResponse) => { - eventCallbackResponse.statusCode = 200; - eventCallbackResponse.setHeader('connection', 'keep-alive'); - - const callbackListener = (data: string): void => { - eventCallbackResponse.write(data.concat('\n'), 'utf8'); - }; - - eventCallbackListeners.add(callbackListener); - - eventCallbackRequest.on('close', () => { - eventCallbackListeners.delete(callbackListener); - }); - - eventCallbackRequest.on('error', () => { - eventCallbackListeners.delete(callbackListener); - }); - }); - - const eventCallbackServerStartupPromise = new Promise(resolve => { - eventCallbackServer.listen(0, () => { - const port = String((eventCallbackServer.address() as AddressInfo).port); - void registerCallbackServerPort(options.proxyServerName, port).then(resolve); - }); - }); - - await eventCallbackServerStartupPromise; - await proxyServerStartupPromise; - return; -} - -export async function waitForRequest( - proxyServerName: string, - callback: (eventData: SentryRequestCallbackData) => Promise | boolean, -): Promise { - const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName); - - return new Promise((resolve, reject) => { - const request = http.request(`http://localhost:${eventCallbackServerPort}/`, {}, response => { - let eventContents = ''; - - response.on('error', err => { - reject(err); - }); - - response.on('data', (chunk: Buffer) => { - const chunkString = chunk.toString('utf8'); - chunkString.split('').forEach(char => { - if (char === '\n') { - const eventCallbackData: SentryRequestCallbackData = JSON.parse( - Buffer.from(eventContents, 'base64').toString('utf8'), - ); - const callbackResult = callback(eventCallbackData); - if (typeof callbackResult !== 'boolean') { - callbackResult.then( - match => { - if (match) { - response.destroy(); - resolve(eventCallbackData); - } - }, - err => { - throw err; - }, - ); - } else if (callbackResult) { - response.destroy(); - resolve(eventCallbackData); - } - eventContents = ''; - } else { - eventContents = eventContents.concat(char); - } - }); - }); - }); - - request.end(); - }); -} - -export function waitForEnvelopeItem( - proxyServerName: string, - callback: (envelopeItem: EnvelopeItem) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForRequest(proxyServerName, async eventData => { - const envelopeItems = eventData.envelope[1]; - for (const envelopeItem of envelopeItems) { - if (await callback(envelopeItem)) { - resolve(envelopeItem); - return true; - } - } - return false; - }).catch(reject); - }); -} - -export function waitForError( - proxyServerName: string, - callback: (transactionEvent: Event) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForEnvelopeItem(proxyServerName, async envelopeItem => { - const [envelopeItemHeader, envelopeItemBody] = envelopeItem; - if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) { - resolve(envelopeItemBody as Event); - return true; - } - return false; - }).catch(reject); - }); -} - -export function waitForTransaction( - proxyServerName: string, - callback: (transactionEvent: Event) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForEnvelopeItem(proxyServerName, async envelopeItem => { - const [envelopeItemHeader, envelopeItemBody] = envelopeItem; - if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) { - resolve(envelopeItemBody as Event); - return true; - } - return false; - }).catch(reject); - }); -} - -const TEMP_FILE_PREFIX = 'event-proxy-server-'; - -async function registerCallbackServerPort(serverName: string, port: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - await writeFile(tmpFilePath, port, { encoding: 'utf8' }); -} - -function retrieveCallbackServerPort(serverName: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - return readFile(tmpFilePath, 'utf8'); -} diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app/package.json index 365fd9fb0bac..67d2f5ba4564 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app/package.json @@ -21,6 +21,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", "@playwright/test": "^1.36.2", "@remix-run/dev": "^1.19.3", "@remix-run/eslint-config": "^1.19.3", diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/create-remix-app/start-event-proxy.ts index 93755c9d232e..55a03fbb64d9 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app/start-event-proxy.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app/start-event-proxy.ts @@ -1,4 +1,4 @@ -import { startEventProxyServer } from './event-proxy-server'; +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; startEventProxyServer({ port: 3031, proxyServerName: 'create-remix-app', diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app/tests/behaviour-server.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app/tests/behaviour-server.test.ts index d0d737e44a69..3001f3c559ff 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app/tests/behaviour-server.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app/tests/behaviour-server.test.ts @@ -1,7 +1,7 @@ import { expect, test } from '@playwright/test'; import { uuid4 } from '@sentry/utils'; -import { waitForTransaction } from '../event-proxy-server'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; test('Sends two linked transactions (server & client) to Sentry', async ({ page }) => { // We use this to identify the transactions diff --git a/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/.npmrc b/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/package.json b/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/package.json new file mode 100644 index 000000000000..70055cdf8159 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/package.json @@ -0,0 +1,28 @@ +{ + "name": "node-express-app", + "version": "1.0.0", + "private": true, + "scripts": { + "start": "node --import=@sentry/node/register src/app.mjs", + "clean": "npx rimraf node_modules,pnpm-lock.yaml", + "test:build": "pnpm install", + "test:assert": "playwright test" + }, + "dependencies": { + "@sentry/node": "latest || *", + "@sentry/types": "latest || *", + "express": "4.19.2", + "@types/express": "4.17.17", + "@types/node": "18.15.1", + "typescript": "4.9.5" + }, + "devDependencies": { + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", + "@playwright/test": "^1.27.1", + "ts-node": "10.9.1" + }, + "volta": { + "extends": "../../package.json", + "node": "18.19.1" + } +} diff --git a/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/playwright.config.ts b/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/playwright.config.ts new file mode 100644 index 000000000000..8ac853fd11d0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/playwright.config.ts @@ -0,0 +1,68 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; +import { devices } from '@playwright/test'; + +// Fix urls not resolving to localhost on Node v17+ +// See: https://github.com/axios/axios/issues/3821#issuecomment-1413727575 +import { setDefaultResultOrder } from 'dns'; +setDefaultResultOrder('ipv4first'); + +const eventProxyPort = 3031; +const expressPort = 3030; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + testDir: './tests', + /* Maximum time one test can run for. */ + timeout: 150_000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000, + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: 0, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'list', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: `http://localhost:${expressPort}`, + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: [ + { + command: 'pnpm ts-node-script start-event-proxy.ts', + port: eventProxyPort, + }, + { + command: 'pnpm start', + port: expressPort, + stdout: 'pipe', + stderr: 'pipe', + }, + ], +}; + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/src/app.mjs b/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/src/app.mjs new file mode 100644 index 000000000000..8dc36dc7066b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/src/app.mjs @@ -0,0 +1,49 @@ +import * as Sentry from '@sentry/node'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.E2E_TEST_DSN, + debug: true, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1, +}); + +import express from 'express'; + +const app = express(); +const port = 3030; + +app.get('/test-success', function (req, res) { + setTimeout(() => { + res.status(200).end(); + }, 100); +}); + +app.get('/test-transaction/:param', function (req, res) { + setTimeout(() => { + res.status(200).end(); + }, 100); +}); + +app.get('/test-error', function (req, res) { + Sentry.captureException(new Error('This is an error')); + setTimeout(() => { + Sentry.flush(2000).then(() => { + res.status(200).end(); + }); + }, 100); +}); + +Sentry.setupExpressErrorHandler(app); + +// @ts-ignore +app.use(function onError(err, req, res, next) { + // The error id is attached to `res.sentry` to be returned + // and optionally displayed to the user for support. + res.statusCode = 500; + res.end(res.sentry + '\n'); +}); + +app.listen(port, () => { + console.log(`Example app listening on port ${port}`); +}); diff --git a/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/start-event-proxy.ts new file mode 100644 index 000000000000..8ea4d3e9a251 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/start-event-proxy.ts @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'esm-loader-node-express-app', +}); diff --git a/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/tests/server.test.ts b/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/tests/server.test.ts new file mode 100644 index 000000000000..b11e0849f63a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/tests/server.test.ts @@ -0,0 +1,33 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/event-proxy-server'; + +test('Should record exceptions captured inside handlers', async ({ request }) => { + const errorEventPromise = waitForError('esm-loader-node-express-app', errorEvent => { + return !!errorEvent?.exception?.values?.[0]?.value?.includes('This is an error'); + }); + + await request.get('/test-error'); + + await expect(errorEventPromise).resolves.toBeDefined(); +}); + +test('Should record a transaction for a parameterless route', async ({ request }) => { + const transactionEventPromise = waitForTransaction('esm-loader-node-express-app', transactionEvent => { + console.log('txn', transactionEvent.transaction); + return transactionEvent?.transaction === 'GET /test-success'; + }); + + await request.get('/test-success'); + + await expect(transactionEventPromise).resolves.toBeDefined(); +}); + +test('Should record a transaction for route with aparameters', async ({ request }) => { + const transactionEventPromise = waitForTransaction('esm-loader-node-express-app', transactionEvent => { + return transactionEvent?.transaction === 'GET /test-transaction/:param'; + }); + + await request.get('/test-transaction/1'); + + await expect(transactionEventPromise).resolves.toBeDefined(); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/event-proxy-server.ts b/dev-packages/e2e-tests/test-applications/nextjs-14/event-proxy-server.ts deleted file mode 100644 index d14ca5cb5e72..000000000000 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/event-proxy-server.ts +++ /dev/null @@ -1,253 +0,0 @@ -import * as fs from 'fs'; -import * as http from 'http'; -import * as https from 'https'; -import type { AddressInfo } from 'net'; -import * as os from 'os'; -import * as path from 'path'; -import * as util from 'util'; -import * as zlib from 'zlib'; -import type { Envelope, EnvelopeItem, Event } from '@sentry/types'; -import { parseEnvelope } from '@sentry/utils'; - -const readFile = util.promisify(fs.readFile); -const writeFile = util.promisify(fs.writeFile); - -interface EventProxyServerOptions { - /** Port to start the event proxy server at. */ - port: number; - /** The name for the proxy server used for referencing it with listener functions */ - proxyServerName: string; -} - -interface SentryRequestCallbackData { - envelope: Envelope; - rawProxyRequestBody: string; - rawSentryResponseBody: string; - sentryResponseStatusCode?: number; -} - -/** - * Starts an event proxy server that will proxy events to sentry when the `tunnel` option is used. Point the `tunnel` - * option to this server (like this `tunnel: http://localhost:${port option}/`). - */ -export async function startEventProxyServer(options: EventProxyServerOptions): Promise { - const eventCallbackListeners: Set<(data: string) => void> = new Set(); - - const proxyServer = http.createServer((proxyRequest, proxyResponse) => { - const proxyRequestChunks: Uint8Array[] = []; - - proxyRequest.addListener('data', (chunk: Buffer) => { - proxyRequestChunks.push(chunk); - }); - - proxyRequest.addListener('error', err => { - throw err; - }); - - proxyRequest.addListener('end', () => { - const proxyRequestBody = - proxyRequest.headers['content-encoding'] === 'gzip' - ? zlib.gunzipSync(Buffer.concat(proxyRequestChunks)).toString() - : Buffer.concat(proxyRequestChunks).toString(); - - let envelopeHeader = JSON.parse(proxyRequestBody.split('\n')[0]); - - if (!envelopeHeader.dsn) { - throw new Error('[event-proxy-server] No dsn on envelope header. Please set tunnel option.'); - } - - const { origin, pathname, host } = new URL(envelopeHeader.dsn); - - const projectId = pathname.substring(1); - const sentryIngestUrl = `${origin}/api/${projectId}/envelope/`; - - proxyRequest.headers.host = host; - - const sentryResponseChunks: Uint8Array[] = []; - - const sentryRequest = https.request( - sentryIngestUrl, - { headers: proxyRequest.headers, method: proxyRequest.method }, - sentryResponse => { - sentryResponse.addListener('data', (chunk: Buffer) => { - proxyResponse.write(chunk, 'binary'); - sentryResponseChunks.push(chunk); - }); - - sentryResponse.addListener('end', () => { - eventCallbackListeners.forEach(listener => { - const rawSentryResponseBody = Buffer.concat(sentryResponseChunks).toString(); - - const data: SentryRequestCallbackData = { - envelope: parseEnvelope(proxyRequestBody), - rawProxyRequestBody: proxyRequestBody, - rawSentryResponseBody, - sentryResponseStatusCode: sentryResponse.statusCode, - }; - - listener(Buffer.from(JSON.stringify(data)).toString('base64')); - }); - proxyResponse.end(); - }); - - sentryResponse.addListener('error', err => { - throw err; - }); - - proxyResponse.writeHead(sentryResponse.statusCode || 500, sentryResponse.headers); - }, - ); - - sentryRequest.write(Buffer.concat(proxyRequestChunks), 'binary'); - sentryRequest.end(); - }); - }); - - const proxyServerStartupPromise = new Promise(resolve => { - proxyServer.listen(options.port, () => { - resolve(); - }); - }); - - const eventCallbackServer = http.createServer((eventCallbackRequest, eventCallbackResponse) => { - eventCallbackResponse.statusCode = 200; - eventCallbackResponse.setHeader('connection', 'keep-alive'); - - const callbackListener = (data: string): void => { - eventCallbackResponse.write(data.concat('\n'), 'utf8'); - }; - - eventCallbackListeners.add(callbackListener); - - eventCallbackRequest.on('close', () => { - eventCallbackListeners.delete(callbackListener); - }); - - eventCallbackRequest.on('error', () => { - eventCallbackListeners.delete(callbackListener); - }); - }); - - const eventCallbackServerStartupPromise = new Promise(resolve => { - eventCallbackServer.listen(0, () => { - const port = String((eventCallbackServer.address() as AddressInfo).port); - void registerCallbackServerPort(options.proxyServerName, port).then(resolve); - }); - }); - - await eventCallbackServerStartupPromise; - await proxyServerStartupPromise; - return; -} - -export async function waitForRequest( - proxyServerName: string, - callback: (eventData: SentryRequestCallbackData) => Promise | boolean, -): Promise { - const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName); - - return new Promise((resolve, reject) => { - const request = http.request(`http://localhost:${eventCallbackServerPort}/`, {}, response => { - let eventContents = ''; - - response.on('error', err => { - reject(err); - }); - - response.on('data', (chunk: Buffer) => { - const chunkString = chunk.toString('utf8'); - chunkString.split('').forEach(char => { - if (char === '\n') { - const eventCallbackData: SentryRequestCallbackData = JSON.parse( - Buffer.from(eventContents, 'base64').toString('utf8'), - ); - const callbackResult = callback(eventCallbackData); - if (typeof callbackResult !== 'boolean') { - callbackResult.then( - match => { - if (match) { - response.destroy(); - resolve(eventCallbackData); - } - }, - err => { - throw err; - }, - ); - } else if (callbackResult) { - response.destroy(); - resolve(eventCallbackData); - } - eventContents = ''; - } else { - eventContents = eventContents.concat(char); - } - }); - }); - }); - - request.end(); - }); -} - -export function waitForEnvelopeItem( - proxyServerName: string, - callback: (envelopeItem: EnvelopeItem) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForRequest(proxyServerName, async eventData => { - const envelopeItems = eventData.envelope[1]; - for (const envelopeItem of envelopeItems) { - if (await callback(envelopeItem)) { - resolve(envelopeItem); - return true; - } - } - return false; - }).catch(reject); - }); -} - -export function waitForError( - proxyServerName: string, - callback: (transactionEvent: Event) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForEnvelopeItem(proxyServerName, async envelopeItem => { - const [envelopeItemHeader, envelopeItemBody] = envelopeItem; - if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) { - resolve(envelopeItemBody as Event); - return true; - } - return false; - }).catch(reject); - }); -} - -export function waitForTransaction( - proxyServerName: string, - callback: (transactionEvent: Event) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForEnvelopeItem(proxyServerName, async envelopeItem => { - const [envelopeItemHeader, envelopeItemBody] = envelopeItem; - if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) { - resolve(envelopeItemBody as Event); - return true; - } - return false; - }).catch(reject); - }); -} - -const TEMP_FILE_PREFIX = 'event-proxy-server-'; - -async function registerCallbackServerPort(serverName: string, port: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - await writeFile(tmpFilePath, port, { encoding: 'utf8' }); -} - -function retrieveCallbackServerPort(serverName: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - return readFile(tmpFilePath, 'utf8'); -} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/package.json b/dev-packages/e2e-tests/test-applications/nextjs-14/package.json index d0c41456d260..cb4fd020441d 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/package.json @@ -26,9 +26,10 @@ "wait-port": "1.0.4" }, "devDependencies": { + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", "@sentry-internal/feedback": "latest || *", "@sentry-internal/replay-canvas": "latest || *", - "@sentry-internal/tracing": "latest || *", + "@sentry-internal/browser-utils": "latest || *", "@sentry/browser": "latest || *", "@sentry/core": "latest || *", "@sentry/nextjs": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/nextjs-14/start-event-proxy.ts index eb83fd6fb82d..476672c34359 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/start-event-proxy.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/start-event-proxy.ts @@ -1,4 +1,4 @@ -import { startEventProxyServer } from './event-proxy-server'; +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; startEventProxyServer({ port: 3031, diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts index e52dde8db258..52c28e1d974a 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '../event-proxy-server'; +import { waitForError, waitForTransaction } from '@sentry-internal/event-proxy-server'; test('Should send a transaction event for a generateMetadata() function invokation', async ({ page }) => { const testTitle = 'foobarasdf'; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/tests/request-instrumentation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-14/tests/request-instrumentation.test.ts index cba6fd0f8699..c0a24f747d56 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/tests/request-instrumentation.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/tests/request-instrumentation.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '../event-proxy-server'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; test('Should send a transaction with a fetch span', async ({ page }) => { const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/event-proxy-server.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/event-proxy-server.ts deleted file mode 100644 index 9d839e6c197b..000000000000 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/event-proxy-server.ts +++ /dev/null @@ -1,253 +0,0 @@ -import * as fs from 'fs'; -import * as http from 'http'; -import * as https from 'https'; -import type { AddressInfo } from 'net'; -import * as os from 'os'; -import * as path from 'path'; -import * as util from 'util'; -import * as zlib from 'zlib'; -import type { Envelope, EnvelopeItem, Event } from '@sentry/types'; -import { parseEnvelope } from '@sentry/utils'; - -const readFile = util.promisify(fs.readFile); -const writeFile = util.promisify(fs.writeFile); - -interface EventProxyServerOptions { - /** Port to start the event proxy server at. */ - port: number; - /** The name for the proxy server used for referencing it with listener functions */ - proxyServerName: string; -} - -interface SentryRequestCallbackData { - envelope: Envelope; - rawProxyRequestBody: string; - rawSentryResponseBody: string; - sentryResponseStatusCode?: number; -} - -/** - * Starts an event proxy server that will proxy events to sentry when the `tunnel` option is used. Point the `tunnel` - * option to this server (like this `tunnel: http://localhost:${port option}/`). - */ -export async function startEventProxyServer(options: EventProxyServerOptions): Promise { - const eventCallbackListeners: Set<(data: string) => void> = new Set(); - - const proxyServer = http.createServer((proxyRequest, proxyResponse) => { - const proxyRequestChunks: Uint8Array[] = []; - - proxyRequest.addListener('data', (chunk: Buffer) => { - proxyRequestChunks.push(chunk); - }); - - proxyRequest.addListener('error', err => { - throw err; - }); - - proxyRequest.addListener('end', () => { - const proxyRequestBody = - proxyRequest.headers['content-encoding'] === 'gzip' - ? zlib.gunzipSync(Buffer.concat(proxyRequestChunks)).toString() - : Buffer.concat(proxyRequestChunks).toString(); - - let envelopeHeader = JSON.parse(proxyRequestBody.split('\n')[0]); - - if (!envelopeHeader.dsn) { - throw new Error('[event-proxy-server] No dsn on envelope header. Please set tunnel option.'); - } - - const { origin, pathname, host } = new URL(envelopeHeader.dsn); - - const projectId = pathname.substring(1); - const sentryIngestUrl = `${origin}/api/${projectId}/envelope/`; - - proxyRequest.headers.host = host; - - const sentryResponseChunks: Uint8Array[] = []; - - const sentryRequest = https.request( - sentryIngestUrl, - { headers: proxyRequest.headers, method: proxyRequest.method }, - sentryResponse => { - sentryResponse.addListener('data', (chunk: Buffer) => { - proxyResponse.write(chunk, 'binary'); - sentryResponseChunks.push(chunk); - }); - - sentryResponse.addListener('end', () => { - eventCallbackListeners.forEach(listener => { - const rawSentryResponseBody = Buffer.concat(sentryResponseChunks).toString(); - - const data: SentryRequestCallbackData = { - envelope: parseEnvelope(proxyRequestBody), - rawProxyRequestBody: proxyRequestBody, - rawSentryResponseBody, - sentryResponseStatusCode: sentryResponse.statusCode, - }; - - listener(Buffer.from(JSON.stringify(data)).toString('base64')); - }); - proxyResponse.end(); - }); - - sentryResponse.addListener('error', err => { - throw err; - }); - - proxyResponse.writeHead(sentryResponse.statusCode || 500, sentryResponse.headers); - }, - ); - - sentryRequest.write(Buffer.concat(proxyRequestChunks), 'binary'); - sentryRequest.end(); - }); - }); - - const proxyServerStartupPromise = new Promise(resolve => { - proxyServer.listen(options.port, () => { - resolve(); - }); - }); - - const eventCallbackServer = http.createServer((eventCallbackRequest, eventCallbackResponse) => { - eventCallbackResponse.statusCode = 200; - eventCallbackResponse.setHeader('connection', 'keep-alive'); - - const callbackListener = (data: string): void => { - eventCallbackResponse.write(data.concat('\n'), 'utf8'); - }; - - eventCallbackListeners.add(callbackListener); - - eventCallbackRequest.on('close', () => { - eventCallbackListeners.delete(callbackListener); - }); - - eventCallbackRequest.on('error', () => { - eventCallbackListeners.delete(callbackListener); - }); - }); - - const eventCallbackServerStartupPromise = new Promise(resolve => { - eventCallbackServer.listen(0, () => { - const port = String((eventCallbackServer.address() as AddressInfo).port); - void registerCallbackServerPort(options.proxyServerName, port).then(resolve); - }); - }); - - await eventCallbackServerStartupPromise; - await proxyServerStartupPromise; - return; -} - -export async function waitForRequest( - proxyServerName: string, - callback: (eventData: SentryRequestCallbackData) => Promise | boolean, -): Promise { - const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName); - - return new Promise((resolve, reject) => { - const request = http.request(`http://127.0.0.1:${eventCallbackServerPort}/`, {}, response => { - let eventContents = ''; - - response.on('error', err => { - reject(err); - }); - - response.on('data', (chunk: Buffer) => { - const chunkString = chunk.toString('utf8'); - chunkString.split('').forEach(char => { - if (char === '\n') { - const eventCallbackData: SentryRequestCallbackData = JSON.parse( - Buffer.from(eventContents, 'base64').toString('utf8'), - ); - const callbackResult = callback(eventCallbackData); - if (typeof callbackResult !== 'boolean') { - callbackResult.then( - match => { - if (match) { - response.destroy(); - resolve(eventCallbackData); - } - }, - err => { - throw err; - }, - ); - } else if (callbackResult) { - response.destroy(); - resolve(eventCallbackData); - } - eventContents = ''; - } else { - eventContents = eventContents.concat(char); - } - }); - }); - }); - - request.end(); - }); -} - -export function waitForEnvelopeItem( - proxyServerName: string, - callback: (envelopeItem: EnvelopeItem) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForRequest(proxyServerName, async eventData => { - const envelopeItems = eventData.envelope[1]; - for (const envelopeItem of envelopeItems) { - if (await callback(envelopeItem)) { - resolve(envelopeItem); - return true; - } - } - return false; - }).catch(reject); - }); -} - -export function waitForError( - proxyServerName: string, - callback: (transactionEvent: Event) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForEnvelopeItem(proxyServerName, async envelopeItem => { - const [envelopeItemHeader, envelopeItemBody] = envelopeItem; - if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) { - resolve(envelopeItemBody as Event); - return true; - } - return false; - }).catch(reject); - }); -} - -export function waitForTransaction( - proxyServerName: string, - callback: (transactionEvent: Event) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForEnvelopeItem(proxyServerName, async envelopeItem => { - const [envelopeItemHeader, envelopeItemBody] = envelopeItem; - if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) { - resolve(envelopeItemBody as Event); - return true; - } - return false; - }).catch(reject); - }); -} - -const TEMP_FILE_PREFIX = 'event-proxy-server-'; - -async function registerCallbackServerPort(serverName: string, port: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - await writeFile(tmpFilePath, port, { encoding: 'utf8' }); -} - -function retrieveCallbackServerPort(serverName: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - return readFile(tmpFilePath, 'utf8'); -} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json index 2a9377a52319..4bfb163d9885 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json @@ -29,9 +29,10 @@ "@playwright/test": "^1.27.1" }, "devDependencies": { + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", "@sentry-internal/feedback": "latest || *", "@sentry-internal/replay-canvas": "latest || *", - "@sentry-internal/tracing": "latest || *", + "@sentry-internal/browser-utils": "latest || *", "@sentry/browser": "latest || *", "@sentry/core": "latest || *", "@sentry/nextjs": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/start-event-proxy.ts index ef8d204c5224..d908b1d1c737 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/start-event-proxy.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/start-event-proxy.ts @@ -1,4 +1,4 @@ -import { startEventProxyServer } from './event-proxy-server'; +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; startEventProxyServer({ port: 3031, diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts index 1465c560a36c..4696534e1733 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '../event-proxy-server'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; test('Should allow for async context isolation in the edge SDK', async ({ request }) => { // test.skip(process.env.TEST_ENV === 'development', "Doesn't work in dev mode."); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts index d52cd4f18893..6fd69315b264 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '../event-proxy-server'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; test('Creates a pageload transaction for app router routes', async ({ page }) => { const randomRoute = String(Math.random()); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts index 4acc41814d3c..c63348304cda 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '../event-proxy-server'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; test('Will capture a connected trace for all server components and generation functions when visiting a page', async ({ page, diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/devErrorSymbolification.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/devErrorSymbolification.test.ts index 09e8db8b2abf..cb2612c09403 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/devErrorSymbolification.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/devErrorSymbolification.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError } from '../event-proxy-server'; +import { waitForError } from '@sentry-internal/event-proxy-server'; test.describe('dev mode error symbolification', () => { if (process.env.TEST_ENV !== 'development') { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts index 9c19506392f8..cbe9dcafae71 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '../event-proxy-server'; +import { waitForError, waitForTransaction } from '@sentry-internal/event-proxy-server'; test('Should create a transaction for edge routes', async ({ request }) => { const edgerouteTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge.test.ts index 83dc125f2d4d..4e69abbdd3e2 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '../event-proxy-server'; +import { waitForError, waitForTransaction } from '@sentry-internal/event-proxy-server'; test('Should record exceptions for faulty edge server components', async ({ page }) => { const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/exceptions.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/exceptions.test.ts index 6e50d10fb333..b47df261ceec 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/exceptions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/exceptions.test.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/event-proxy-server'; import axios, { AxiosError } from 'axios'; -import { waitForError } from '../event-proxy-server'; const authToken = process.env.E2E_TEST_AUTH_TOKEN; const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; @@ -19,6 +19,8 @@ test('Sends a client-side exception to Sentry', async ({ page }) => { const errorEvent = await errorEventPromise; const exceptionEventId = errorEvent.event_id; + expect(errorEvent.transaction).toBe('/'); + await expect .poll( async () => { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts index 2dd20e173d98..240e04ebe37f 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '../event-proxy-server'; +import { waitForError, waitForTransaction } from '@sentry-internal/event-proxy-server'; test('Should create a transaction for middleware', async ({ request }) => { const middlewareTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts index 2dbefd1441e2..73f8bd5e31b9 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '../event-proxy-server'; +import { waitForError, waitForTransaction } from '@sentry-internal/event-proxy-server'; test('Will capture error for SSR rendering error with a connected trace (Class Component)', async ({ page }) => { const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/request-instrumentation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/request-instrumentation.test.ts index 6ee318fe8e91..bd6a27cecced 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/request-instrumentation.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/request-instrumentation.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '../event-proxy-server'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; // Note(lforst): I officially declare bancruptcy on this test. I tried a million ways to make it work but it kept flaking. // Sometimes the request span was included in the handler span, more often it wasn't. I have no idea why. Maybe one day we will diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts index 9bb784c39271..70f9bb32d3bc 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '../event-proxy-server'; +import { waitForError, waitForTransaction } from '@sentry-internal/event-proxy-server'; test('Should create a transaction for route handlers', async ({ request }) => { const routehandlerTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts index 3e146433defc..57ddb57f75cf 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; import axios, { AxiosError } from 'axios'; -import { waitForTransaction } from '../event-proxy-server'; const packageJson = require('../package.json'); diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/package.json b/dev-packages/e2e-tests/test-applications/node-exports-test-app/package.json index f8576bb04812..2881c1aab005 100644 --- a/dev-packages/e2e-tests/test-applications/node-exports-test-app/package.json +++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/package.json @@ -12,7 +12,6 @@ "test:assert": "pnpm test" }, "dependencies": { - "@sentry/node-experimental": "latest || *", "@sentry/node": "latest || *", "@sentry/sveltekit": "latest || *", "@sentry/remix": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts index 899cffb979a4..0aceb4418ddc 100644 --- a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts +++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts @@ -2,7 +2,6 @@ import * as SentryAstro from '@sentry/astro'; import * as SentryBun from '@sentry/bun'; import * as SentryNextJs from '@sentry/nextjs'; import * as SentryNode from '@sentry/node'; -import * as SentryNodeExperimental from '@sentry/node-experimental'; import * as SentryRemix from '@sentry/remix'; import * as SentrySvelteKit from '@sentry/sveltekit'; @@ -10,16 +9,6 @@ import * as SentrySvelteKit from '@sentry/sveltekit'; const SentryAWS = require('@sentry/aws-serverless'); const SentryGoogleCloud = require('@sentry/google-cloud-serverless'); -/* List of exports that are safe to ignore / we don't require in any depending package */ -const NODE_EXPERIMENTAL_EXPORTS_IGNORE = [ - 'default', - // Probably generated by transpilation, no need to require it - '__esModule', - // These are not re-exported where not needed - 'Http', - 'Undici', -]; - /* List of exports that are safe to ignore / we don't require in any depending package */ const NODE_EXPORTS_IGNORE = [ 'default', @@ -27,10 +16,6 @@ const NODE_EXPORTS_IGNORE = [ '__esModule', ]; -/* Sanitized list of node exports */ -const nodeExperimentalExports = Object.keys(SentryNodeExperimental).filter( - e => !NODE_EXPERIMENTAL_EXPORTS_IGNORE.includes(e), -); const nodeExports = Object.keys(SentryNode).filter(e => !NODE_EXPORTS_IGNORE.includes(e)); type Dependent = { @@ -114,7 +99,7 @@ for (const dependent of dependentsToCheck) { } if (Object.keys(missingExports).length > 0) { - console.error('\n❌ Found missing exports from @sentry/node in the following packages:\n'); + console.log('\n❌ Found missing exports from @sentry/node in the following packages:\n'); console.log(JSON.stringify(missingExports, null, 2)); process.exit(1); } diff --git a/dev-packages/e2e-tests/test-applications/node-express-app/event-proxy-server.ts b/dev-packages/e2e-tests/test-applications/node-express-app/event-proxy-server.ts deleted file mode 100644 index d14ca5cb5e72..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-express-app/event-proxy-server.ts +++ /dev/null @@ -1,253 +0,0 @@ -import * as fs from 'fs'; -import * as http from 'http'; -import * as https from 'https'; -import type { AddressInfo } from 'net'; -import * as os from 'os'; -import * as path from 'path'; -import * as util from 'util'; -import * as zlib from 'zlib'; -import type { Envelope, EnvelopeItem, Event } from '@sentry/types'; -import { parseEnvelope } from '@sentry/utils'; - -const readFile = util.promisify(fs.readFile); -const writeFile = util.promisify(fs.writeFile); - -interface EventProxyServerOptions { - /** Port to start the event proxy server at. */ - port: number; - /** The name for the proxy server used for referencing it with listener functions */ - proxyServerName: string; -} - -interface SentryRequestCallbackData { - envelope: Envelope; - rawProxyRequestBody: string; - rawSentryResponseBody: string; - sentryResponseStatusCode?: number; -} - -/** - * Starts an event proxy server that will proxy events to sentry when the `tunnel` option is used. Point the `tunnel` - * option to this server (like this `tunnel: http://localhost:${port option}/`). - */ -export async function startEventProxyServer(options: EventProxyServerOptions): Promise { - const eventCallbackListeners: Set<(data: string) => void> = new Set(); - - const proxyServer = http.createServer((proxyRequest, proxyResponse) => { - const proxyRequestChunks: Uint8Array[] = []; - - proxyRequest.addListener('data', (chunk: Buffer) => { - proxyRequestChunks.push(chunk); - }); - - proxyRequest.addListener('error', err => { - throw err; - }); - - proxyRequest.addListener('end', () => { - const proxyRequestBody = - proxyRequest.headers['content-encoding'] === 'gzip' - ? zlib.gunzipSync(Buffer.concat(proxyRequestChunks)).toString() - : Buffer.concat(proxyRequestChunks).toString(); - - let envelopeHeader = JSON.parse(proxyRequestBody.split('\n')[0]); - - if (!envelopeHeader.dsn) { - throw new Error('[event-proxy-server] No dsn on envelope header. Please set tunnel option.'); - } - - const { origin, pathname, host } = new URL(envelopeHeader.dsn); - - const projectId = pathname.substring(1); - const sentryIngestUrl = `${origin}/api/${projectId}/envelope/`; - - proxyRequest.headers.host = host; - - const sentryResponseChunks: Uint8Array[] = []; - - const sentryRequest = https.request( - sentryIngestUrl, - { headers: proxyRequest.headers, method: proxyRequest.method }, - sentryResponse => { - sentryResponse.addListener('data', (chunk: Buffer) => { - proxyResponse.write(chunk, 'binary'); - sentryResponseChunks.push(chunk); - }); - - sentryResponse.addListener('end', () => { - eventCallbackListeners.forEach(listener => { - const rawSentryResponseBody = Buffer.concat(sentryResponseChunks).toString(); - - const data: SentryRequestCallbackData = { - envelope: parseEnvelope(proxyRequestBody), - rawProxyRequestBody: proxyRequestBody, - rawSentryResponseBody, - sentryResponseStatusCode: sentryResponse.statusCode, - }; - - listener(Buffer.from(JSON.stringify(data)).toString('base64')); - }); - proxyResponse.end(); - }); - - sentryResponse.addListener('error', err => { - throw err; - }); - - proxyResponse.writeHead(sentryResponse.statusCode || 500, sentryResponse.headers); - }, - ); - - sentryRequest.write(Buffer.concat(proxyRequestChunks), 'binary'); - sentryRequest.end(); - }); - }); - - const proxyServerStartupPromise = new Promise(resolve => { - proxyServer.listen(options.port, () => { - resolve(); - }); - }); - - const eventCallbackServer = http.createServer((eventCallbackRequest, eventCallbackResponse) => { - eventCallbackResponse.statusCode = 200; - eventCallbackResponse.setHeader('connection', 'keep-alive'); - - const callbackListener = (data: string): void => { - eventCallbackResponse.write(data.concat('\n'), 'utf8'); - }; - - eventCallbackListeners.add(callbackListener); - - eventCallbackRequest.on('close', () => { - eventCallbackListeners.delete(callbackListener); - }); - - eventCallbackRequest.on('error', () => { - eventCallbackListeners.delete(callbackListener); - }); - }); - - const eventCallbackServerStartupPromise = new Promise(resolve => { - eventCallbackServer.listen(0, () => { - const port = String((eventCallbackServer.address() as AddressInfo).port); - void registerCallbackServerPort(options.proxyServerName, port).then(resolve); - }); - }); - - await eventCallbackServerStartupPromise; - await proxyServerStartupPromise; - return; -} - -export async function waitForRequest( - proxyServerName: string, - callback: (eventData: SentryRequestCallbackData) => Promise | boolean, -): Promise { - const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName); - - return new Promise((resolve, reject) => { - const request = http.request(`http://localhost:${eventCallbackServerPort}/`, {}, response => { - let eventContents = ''; - - response.on('error', err => { - reject(err); - }); - - response.on('data', (chunk: Buffer) => { - const chunkString = chunk.toString('utf8'); - chunkString.split('').forEach(char => { - if (char === '\n') { - const eventCallbackData: SentryRequestCallbackData = JSON.parse( - Buffer.from(eventContents, 'base64').toString('utf8'), - ); - const callbackResult = callback(eventCallbackData); - if (typeof callbackResult !== 'boolean') { - callbackResult.then( - match => { - if (match) { - response.destroy(); - resolve(eventCallbackData); - } - }, - err => { - throw err; - }, - ); - } else if (callbackResult) { - response.destroy(); - resolve(eventCallbackData); - } - eventContents = ''; - } else { - eventContents = eventContents.concat(char); - } - }); - }); - }); - - request.end(); - }); -} - -export function waitForEnvelopeItem( - proxyServerName: string, - callback: (envelopeItem: EnvelopeItem) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForRequest(proxyServerName, async eventData => { - const envelopeItems = eventData.envelope[1]; - for (const envelopeItem of envelopeItems) { - if (await callback(envelopeItem)) { - resolve(envelopeItem); - return true; - } - } - return false; - }).catch(reject); - }); -} - -export function waitForError( - proxyServerName: string, - callback: (transactionEvent: Event) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForEnvelopeItem(proxyServerName, async envelopeItem => { - const [envelopeItemHeader, envelopeItemBody] = envelopeItem; - if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) { - resolve(envelopeItemBody as Event); - return true; - } - return false; - }).catch(reject); - }); -} - -export function waitForTransaction( - proxyServerName: string, - callback: (transactionEvent: Event) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForEnvelopeItem(proxyServerName, async envelopeItem => { - const [envelopeItemHeader, envelopeItemBody] = envelopeItem; - if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) { - resolve(envelopeItemBody as Event); - return true; - } - return false; - }).catch(reject); - }); -} - -const TEMP_FILE_PREFIX = 'event-proxy-server-'; - -async function registerCallbackServerPort(serverName: string, port: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - await writeFile(tmpFilePath, port, { encoding: 'utf8' }); -} - -function retrieveCallbackServerPort(serverName: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - return readFile(tmpFilePath, 'utf8'); -} diff --git a/dev-packages/e2e-tests/test-applications/node-express-app/package.json b/dev-packages/e2e-tests/test-applications/node-express-app/package.json index 9bd84e0e91a2..3ba52ef6f876 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-app/package.json +++ b/dev-packages/e2e-tests/test-applications/node-express-app/package.json @@ -11,14 +11,19 @@ "test:assert": "pnpm test" }, "dependencies": { + "@sentry/core": "latest || *", "@sentry/node": "latest || *", "@sentry/types": "latest || *", - "express": "4.19.2", + "@trpc/server": "10.45.2", + "@trpc/client": "10.45.2", "@types/express": "4.17.17", "@types/node": "18.15.1", - "typescript": "4.9.5" + "express": "4.19.2", + "typescript": "4.9.5", + "zod": "^3.22.4" }, "devDependencies": { + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", "@playwright/test": "^1.27.1", "ts-node": "10.9.1" }, diff --git a/dev-packages/e2e-tests/test-applications/node-express-app/src/app.ts b/dev-packages/e2e-tests/test-applications/node-express-app/src/app.ts index a4d6ccc6c983..b94a49c2c6dc 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-app/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-app/src/app.ts @@ -1,5 +1,8 @@ import * as Sentry from '@sentry/node'; +import { TRPCError, initTRPC } from '@trpc/server'; +import * as trpcExpress from '@trpc/server/adapters/express'; import express from 'express'; +import { z } from 'zod'; declare global { namespace globalThis { @@ -94,3 +97,36 @@ Sentry.addEventProcessor(event => { return event; }); + +export const t = initTRPC.context().create(); + +const procedure = t.procedure.use(Sentry.trpcMiddleware({ attachRpcInput: true })); + +export const appRouter = t.router({ + getSomething: procedure.input(z.string()).query(opts => { + return { id: opts.input, name: 'Bilbo' }; + }), + createSomething: procedure.mutation(async () => { + await new Promise(resolve => setTimeout(resolve, 400)); + return { success: true }; + }), + crashSomething: procedure.mutation(() => { + throw new Error('I crashed in a trpc handler'); + }), + dontFindSomething: procedure.mutation(() => { + throw new TRPCError({ code: 'NOT_FOUND', cause: new Error('Page not found') }); + }), +}); + +export type AppRouter = typeof appRouter; + +const createContext = () => ({ someStaticValue: 'asdf' }); +type Context = Awaited>; + +app.use( + '/trpc', + trpcExpress.createExpressMiddleware({ + router: appRouter, + createContext, + }), +); diff --git a/dev-packages/e2e-tests/test-applications/node-express-app/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/node-express-app/start-event-proxy.ts index 376afc851351..369041a9c792 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-app/start-event-proxy.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-app/start-event-proxy.ts @@ -1,4 +1,4 @@ -import { startEventProxyServer } from './event-proxy-server'; +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; startEventProxyServer({ port: 3031, diff --git a/dev-packages/e2e-tests/test-applications/node-express-app/tests/server.test.ts b/dev-packages/e2e-tests/test-applications/node-express-app/tests/server.test.ts index b139a09dd2a6..b08dca4dd299 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-app/tests/server.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-app/tests/server.test.ts @@ -1,6 +1,8 @@ import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/event-proxy-server'; +import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'; import axios, { AxiosError, AxiosResponse } from 'axios'; -import { waitForError } from '../event-proxy-server'; +import type { AppRouter } from '../src/app'; const authToken = process.env.E2E_TEST_AUTH_TOKEN; const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; @@ -130,3 +132,96 @@ test('Should record uncaught exceptions with local variable', async ({ baseURL } expect(frames[frames.length - 1].vars?.randomVariableToRecord).toBeDefined(); }); + +test('Should record transaction for trpc query', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('node-express-app', transactionEvent => { + return transactionEvent.transaction === 'trpc/getSomething'; + }); + + const trpcClient = createTRPCProxyClient({ + links: [ + httpBatchLink({ + url: `${baseURL}/trpc`, + }), + ], + }); + + await trpcClient.getSomething.query('foobar'); + + await expect(transactionEventPromise).resolves.toBeDefined(); + const transaction = await transactionEventPromise; + + expect(transaction.contexts?.trpc).toMatchObject({ + procedure_type: 'query', + input: 'foobar', + }); +}); + +test('Should record transaction for trpc mutation', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('node-express-app', transactionEvent => { + return transactionEvent.transaction === 'trpc/createSomething'; + }); + + const trpcClient = createTRPCProxyClient({ + links: [ + httpBatchLink({ + url: `${baseURL}/trpc`, + }), + ], + }); + + await trpcClient.createSomething.mutate(); + + await expect(transactionEventPromise).resolves.toBeDefined(); + const transaction = await transactionEventPromise; + + expect(transaction.contexts?.trpc).toMatchObject({ + procedure_type: 'mutation', + }); +}); + +test('Should record transaction and error for a crashing trpc handler', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('node-express-app', transactionEvent => { + return transactionEvent.transaction === 'trpc/crashSomething'; + }); + + const errorEventPromise = waitForError('node-express-app', errorEvent => { + return !!errorEvent?.exception?.values?.some(exception => exception.value?.includes('I crashed in a trpc handler')); + }); + + const trpcClient = createTRPCProxyClient({ + links: [ + httpBatchLink({ + url: `${baseURL}/trpc`, + }), + ], + }); + + await expect(trpcClient.crashSomething.mutate()).rejects.toBeDefined(); + + await expect(transactionEventPromise).resolves.toBeDefined(); + await expect(errorEventPromise).resolves.toBeDefined(); +}); + +test('Should record transaction and error for a trpc handler that returns a status code', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('node-express-app', transactionEvent => { + return transactionEvent.transaction === 'trpc/dontFindSomething'; + }); + + const errorEventPromise = waitForError('node-express-app', errorEvent => { + return !!errorEvent?.exception?.values?.some(exception => exception.value?.includes('Page not found')); + }); + + const trpcClient = createTRPCProxyClient({ + links: [ + httpBatchLink({ + url: `${baseURL}/trpc`, + }), + ], + }); + + await expect(trpcClient.dontFindSomething.mutate()).rejects.toBeDefined(); + + await expect(transactionEventPromise).resolves.toBeDefined(); + await expect(errorEventPromise).resolves.toBeDefined(); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-app/event-proxy-server.ts b/dev-packages/e2e-tests/test-applications/node-fastify-app/event-proxy-server.ts deleted file mode 100644 index d14ca5cb5e72..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-fastify-app/event-proxy-server.ts +++ /dev/null @@ -1,253 +0,0 @@ -import * as fs from 'fs'; -import * as http from 'http'; -import * as https from 'https'; -import type { AddressInfo } from 'net'; -import * as os from 'os'; -import * as path from 'path'; -import * as util from 'util'; -import * as zlib from 'zlib'; -import type { Envelope, EnvelopeItem, Event } from '@sentry/types'; -import { parseEnvelope } from '@sentry/utils'; - -const readFile = util.promisify(fs.readFile); -const writeFile = util.promisify(fs.writeFile); - -interface EventProxyServerOptions { - /** Port to start the event proxy server at. */ - port: number; - /** The name for the proxy server used for referencing it with listener functions */ - proxyServerName: string; -} - -interface SentryRequestCallbackData { - envelope: Envelope; - rawProxyRequestBody: string; - rawSentryResponseBody: string; - sentryResponseStatusCode?: number; -} - -/** - * Starts an event proxy server that will proxy events to sentry when the `tunnel` option is used. Point the `tunnel` - * option to this server (like this `tunnel: http://localhost:${port option}/`). - */ -export async function startEventProxyServer(options: EventProxyServerOptions): Promise { - const eventCallbackListeners: Set<(data: string) => void> = new Set(); - - const proxyServer = http.createServer((proxyRequest, proxyResponse) => { - const proxyRequestChunks: Uint8Array[] = []; - - proxyRequest.addListener('data', (chunk: Buffer) => { - proxyRequestChunks.push(chunk); - }); - - proxyRequest.addListener('error', err => { - throw err; - }); - - proxyRequest.addListener('end', () => { - const proxyRequestBody = - proxyRequest.headers['content-encoding'] === 'gzip' - ? zlib.gunzipSync(Buffer.concat(proxyRequestChunks)).toString() - : Buffer.concat(proxyRequestChunks).toString(); - - let envelopeHeader = JSON.parse(proxyRequestBody.split('\n')[0]); - - if (!envelopeHeader.dsn) { - throw new Error('[event-proxy-server] No dsn on envelope header. Please set tunnel option.'); - } - - const { origin, pathname, host } = new URL(envelopeHeader.dsn); - - const projectId = pathname.substring(1); - const sentryIngestUrl = `${origin}/api/${projectId}/envelope/`; - - proxyRequest.headers.host = host; - - const sentryResponseChunks: Uint8Array[] = []; - - const sentryRequest = https.request( - sentryIngestUrl, - { headers: proxyRequest.headers, method: proxyRequest.method }, - sentryResponse => { - sentryResponse.addListener('data', (chunk: Buffer) => { - proxyResponse.write(chunk, 'binary'); - sentryResponseChunks.push(chunk); - }); - - sentryResponse.addListener('end', () => { - eventCallbackListeners.forEach(listener => { - const rawSentryResponseBody = Buffer.concat(sentryResponseChunks).toString(); - - const data: SentryRequestCallbackData = { - envelope: parseEnvelope(proxyRequestBody), - rawProxyRequestBody: proxyRequestBody, - rawSentryResponseBody, - sentryResponseStatusCode: sentryResponse.statusCode, - }; - - listener(Buffer.from(JSON.stringify(data)).toString('base64')); - }); - proxyResponse.end(); - }); - - sentryResponse.addListener('error', err => { - throw err; - }); - - proxyResponse.writeHead(sentryResponse.statusCode || 500, sentryResponse.headers); - }, - ); - - sentryRequest.write(Buffer.concat(proxyRequestChunks), 'binary'); - sentryRequest.end(); - }); - }); - - const proxyServerStartupPromise = new Promise(resolve => { - proxyServer.listen(options.port, () => { - resolve(); - }); - }); - - const eventCallbackServer = http.createServer((eventCallbackRequest, eventCallbackResponse) => { - eventCallbackResponse.statusCode = 200; - eventCallbackResponse.setHeader('connection', 'keep-alive'); - - const callbackListener = (data: string): void => { - eventCallbackResponse.write(data.concat('\n'), 'utf8'); - }; - - eventCallbackListeners.add(callbackListener); - - eventCallbackRequest.on('close', () => { - eventCallbackListeners.delete(callbackListener); - }); - - eventCallbackRequest.on('error', () => { - eventCallbackListeners.delete(callbackListener); - }); - }); - - const eventCallbackServerStartupPromise = new Promise(resolve => { - eventCallbackServer.listen(0, () => { - const port = String((eventCallbackServer.address() as AddressInfo).port); - void registerCallbackServerPort(options.proxyServerName, port).then(resolve); - }); - }); - - await eventCallbackServerStartupPromise; - await proxyServerStartupPromise; - return; -} - -export async function waitForRequest( - proxyServerName: string, - callback: (eventData: SentryRequestCallbackData) => Promise | boolean, -): Promise { - const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName); - - return new Promise((resolve, reject) => { - const request = http.request(`http://localhost:${eventCallbackServerPort}/`, {}, response => { - let eventContents = ''; - - response.on('error', err => { - reject(err); - }); - - response.on('data', (chunk: Buffer) => { - const chunkString = chunk.toString('utf8'); - chunkString.split('').forEach(char => { - if (char === '\n') { - const eventCallbackData: SentryRequestCallbackData = JSON.parse( - Buffer.from(eventContents, 'base64').toString('utf8'), - ); - const callbackResult = callback(eventCallbackData); - if (typeof callbackResult !== 'boolean') { - callbackResult.then( - match => { - if (match) { - response.destroy(); - resolve(eventCallbackData); - } - }, - err => { - throw err; - }, - ); - } else if (callbackResult) { - response.destroy(); - resolve(eventCallbackData); - } - eventContents = ''; - } else { - eventContents = eventContents.concat(char); - } - }); - }); - }); - - request.end(); - }); -} - -export function waitForEnvelopeItem( - proxyServerName: string, - callback: (envelopeItem: EnvelopeItem) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForRequest(proxyServerName, async eventData => { - const envelopeItems = eventData.envelope[1]; - for (const envelopeItem of envelopeItems) { - if (await callback(envelopeItem)) { - resolve(envelopeItem); - return true; - } - } - return false; - }).catch(reject); - }); -} - -export function waitForError( - proxyServerName: string, - callback: (transactionEvent: Event) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForEnvelopeItem(proxyServerName, async envelopeItem => { - const [envelopeItemHeader, envelopeItemBody] = envelopeItem; - if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) { - resolve(envelopeItemBody as Event); - return true; - } - return false; - }).catch(reject); - }); -} - -export function waitForTransaction( - proxyServerName: string, - callback: (transactionEvent: Event) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForEnvelopeItem(proxyServerName, async envelopeItem => { - const [envelopeItemHeader, envelopeItemBody] = envelopeItem; - if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) { - resolve(envelopeItemBody as Event); - return true; - } - return false; - }).catch(reject); - }); -} - -const TEMP_FILE_PREFIX = 'event-proxy-server-'; - -async function registerCallbackServerPort(serverName: string, port: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - await writeFile(tmpFilePath, port, { encoding: 'utf8' }); -} - -function retrieveCallbackServerPort(serverName: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - return readFile(tmpFilePath, 'utf8'); -} diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-app/package.json b/dev-packages/e2e-tests/test-applications/node-fastify-app/package.json index c7ea9cac71ad..9e8779cf9bdd 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-app/package.json +++ b/dev-packages/e2e-tests/test-applications/node-fastify-app/package.json @@ -21,6 +21,7 @@ "ts-node": "10.9.1" }, "devDependencies": { + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", "@playwright/test": "^1.38.1" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-app/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/node-fastify-app/start-event-proxy.ts index 2ab9be450dcd..cb3a189ed920 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-app/start-event-proxy.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-app/start-event-proxy.ts @@ -1,4 +1,4 @@ -import { startEventProxyServer } from './event-proxy-server'; +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; startEventProxyServer({ port: 3031, diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/errors.test.ts index b2ef0649472f..e9ee378fa00f 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/errors.test.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/event-proxy-server'; import axios, { AxiosError } from 'axios'; -import { waitForError } from '../event-proxy-server'; const authToken = process.env.E2E_TEST_AUTH_TOKEN; const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; 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 411863f54cfb..11d8e896b2aa 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,8 +1,8 @@ import crypto from 'crypto'; import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; 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(); diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/transactions.test.ts index 54f6916de36c..6d3ca7e8d6fd 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/transactions.test.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; import axios, { AxiosError } from 'axios'; -import { waitForTransaction } from '../event-proxy-server'; const authToken = process.env.E2E_TEST_AUTH_TOKEN; const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; diff --git a/dev-packages/e2e-tests/test-applications/node-hapi-app/event-proxy-server.ts b/dev-packages/e2e-tests/test-applications/node-hapi-app/event-proxy-server.ts deleted file mode 100644 index d14ca5cb5e72..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-hapi-app/event-proxy-server.ts +++ /dev/null @@ -1,253 +0,0 @@ -import * as fs from 'fs'; -import * as http from 'http'; -import * as https from 'https'; -import type { AddressInfo } from 'net'; -import * as os from 'os'; -import * as path from 'path'; -import * as util from 'util'; -import * as zlib from 'zlib'; -import type { Envelope, EnvelopeItem, Event } from '@sentry/types'; -import { parseEnvelope } from '@sentry/utils'; - -const readFile = util.promisify(fs.readFile); -const writeFile = util.promisify(fs.writeFile); - -interface EventProxyServerOptions { - /** Port to start the event proxy server at. */ - port: number; - /** The name for the proxy server used for referencing it with listener functions */ - proxyServerName: string; -} - -interface SentryRequestCallbackData { - envelope: Envelope; - rawProxyRequestBody: string; - rawSentryResponseBody: string; - sentryResponseStatusCode?: number; -} - -/** - * Starts an event proxy server that will proxy events to sentry when the `tunnel` option is used. Point the `tunnel` - * option to this server (like this `tunnel: http://localhost:${port option}/`). - */ -export async function startEventProxyServer(options: EventProxyServerOptions): Promise { - const eventCallbackListeners: Set<(data: string) => void> = new Set(); - - const proxyServer = http.createServer((proxyRequest, proxyResponse) => { - const proxyRequestChunks: Uint8Array[] = []; - - proxyRequest.addListener('data', (chunk: Buffer) => { - proxyRequestChunks.push(chunk); - }); - - proxyRequest.addListener('error', err => { - throw err; - }); - - proxyRequest.addListener('end', () => { - const proxyRequestBody = - proxyRequest.headers['content-encoding'] === 'gzip' - ? zlib.gunzipSync(Buffer.concat(proxyRequestChunks)).toString() - : Buffer.concat(proxyRequestChunks).toString(); - - let envelopeHeader = JSON.parse(proxyRequestBody.split('\n')[0]); - - if (!envelopeHeader.dsn) { - throw new Error('[event-proxy-server] No dsn on envelope header. Please set tunnel option.'); - } - - const { origin, pathname, host } = new URL(envelopeHeader.dsn); - - const projectId = pathname.substring(1); - const sentryIngestUrl = `${origin}/api/${projectId}/envelope/`; - - proxyRequest.headers.host = host; - - const sentryResponseChunks: Uint8Array[] = []; - - const sentryRequest = https.request( - sentryIngestUrl, - { headers: proxyRequest.headers, method: proxyRequest.method }, - sentryResponse => { - sentryResponse.addListener('data', (chunk: Buffer) => { - proxyResponse.write(chunk, 'binary'); - sentryResponseChunks.push(chunk); - }); - - sentryResponse.addListener('end', () => { - eventCallbackListeners.forEach(listener => { - const rawSentryResponseBody = Buffer.concat(sentryResponseChunks).toString(); - - const data: SentryRequestCallbackData = { - envelope: parseEnvelope(proxyRequestBody), - rawProxyRequestBody: proxyRequestBody, - rawSentryResponseBody, - sentryResponseStatusCode: sentryResponse.statusCode, - }; - - listener(Buffer.from(JSON.stringify(data)).toString('base64')); - }); - proxyResponse.end(); - }); - - sentryResponse.addListener('error', err => { - throw err; - }); - - proxyResponse.writeHead(sentryResponse.statusCode || 500, sentryResponse.headers); - }, - ); - - sentryRequest.write(Buffer.concat(proxyRequestChunks), 'binary'); - sentryRequest.end(); - }); - }); - - const proxyServerStartupPromise = new Promise(resolve => { - proxyServer.listen(options.port, () => { - resolve(); - }); - }); - - const eventCallbackServer = http.createServer((eventCallbackRequest, eventCallbackResponse) => { - eventCallbackResponse.statusCode = 200; - eventCallbackResponse.setHeader('connection', 'keep-alive'); - - const callbackListener = (data: string): void => { - eventCallbackResponse.write(data.concat('\n'), 'utf8'); - }; - - eventCallbackListeners.add(callbackListener); - - eventCallbackRequest.on('close', () => { - eventCallbackListeners.delete(callbackListener); - }); - - eventCallbackRequest.on('error', () => { - eventCallbackListeners.delete(callbackListener); - }); - }); - - const eventCallbackServerStartupPromise = new Promise(resolve => { - eventCallbackServer.listen(0, () => { - const port = String((eventCallbackServer.address() as AddressInfo).port); - void registerCallbackServerPort(options.proxyServerName, port).then(resolve); - }); - }); - - await eventCallbackServerStartupPromise; - await proxyServerStartupPromise; - return; -} - -export async function waitForRequest( - proxyServerName: string, - callback: (eventData: SentryRequestCallbackData) => Promise | boolean, -): Promise { - const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName); - - return new Promise((resolve, reject) => { - const request = http.request(`http://localhost:${eventCallbackServerPort}/`, {}, response => { - let eventContents = ''; - - response.on('error', err => { - reject(err); - }); - - response.on('data', (chunk: Buffer) => { - const chunkString = chunk.toString('utf8'); - chunkString.split('').forEach(char => { - if (char === '\n') { - const eventCallbackData: SentryRequestCallbackData = JSON.parse( - Buffer.from(eventContents, 'base64').toString('utf8'), - ); - const callbackResult = callback(eventCallbackData); - if (typeof callbackResult !== 'boolean') { - callbackResult.then( - match => { - if (match) { - response.destroy(); - resolve(eventCallbackData); - } - }, - err => { - throw err; - }, - ); - } else if (callbackResult) { - response.destroy(); - resolve(eventCallbackData); - } - eventContents = ''; - } else { - eventContents = eventContents.concat(char); - } - }); - }); - }); - - request.end(); - }); -} - -export function waitForEnvelopeItem( - proxyServerName: string, - callback: (envelopeItem: EnvelopeItem) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForRequest(proxyServerName, async eventData => { - const envelopeItems = eventData.envelope[1]; - for (const envelopeItem of envelopeItems) { - if (await callback(envelopeItem)) { - resolve(envelopeItem); - return true; - } - } - return false; - }).catch(reject); - }); -} - -export function waitForError( - proxyServerName: string, - callback: (transactionEvent: Event) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForEnvelopeItem(proxyServerName, async envelopeItem => { - const [envelopeItemHeader, envelopeItemBody] = envelopeItem; - if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) { - resolve(envelopeItemBody as Event); - return true; - } - return false; - }).catch(reject); - }); -} - -export function waitForTransaction( - proxyServerName: string, - callback: (transactionEvent: Event) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForEnvelopeItem(proxyServerName, async envelopeItem => { - const [envelopeItemHeader, envelopeItemBody] = envelopeItem; - if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) { - resolve(envelopeItemBody as Event); - return true; - } - return false; - }).catch(reject); - }); -} - -const TEMP_FILE_PREFIX = 'event-proxy-server-'; - -async function registerCallbackServerPort(serverName: string, port: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - await writeFile(tmpFilePath, port, { encoding: 'utf8' }); -} - -function retrieveCallbackServerPort(serverName: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - return readFile(tmpFilePath, 'utf8'); -} diff --git a/dev-packages/e2e-tests/test-applications/node-hapi-app/package.json b/dev-packages/e2e-tests/test-applications/node-hapi-app/package.json index e463d02a73e6..d63a3a6c457f 100644 --- a/dev-packages/e2e-tests/test-applications/node-hapi-app/package.json +++ b/dev-packages/e2e-tests/test-applications/node-hapi-app/package.json @@ -18,6 +18,7 @@ "typescript": "4.9.5" }, "devDependencies": { + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", "@playwright/test": "^1.27.1", "ts-node": "10.9.1" }, diff --git a/dev-packages/e2e-tests/test-applications/node-hapi-app/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/node-hapi-app/start-event-proxy.ts index 7a3ed463e2ae..f1f5cf4b3316 100644 --- a/dev-packages/e2e-tests/test-applications/node-hapi-app/start-event-proxy.ts +++ b/dev-packages/e2e-tests/test-applications/node-hapi-app/start-event-proxy.ts @@ -1,4 +1,4 @@ -import { startEventProxyServer } from './event-proxy-server'; +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; startEventProxyServer({ port: 3031, diff --git a/dev-packages/e2e-tests/test-applications/node-hapi-app/tests/server.test.ts b/dev-packages/e2e-tests/test-applications/node-hapi-app/tests/server.test.ts index 061278dded50..d50c68ac4e72 100644 --- a/dev-packages/e2e-tests/test-applications/node-hapi-app/tests/server.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-hapi-app/tests/server.test.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/event-proxy-server'; import axios, { AxiosError } from 'axios'; -import { waitForError, waitForTransaction } from '../event-proxy-server'; const authToken = process.env.E2E_TEST_AUTH_TOKEN; const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; diff --git a/dev-packages/e2e-tests/test-applications/node-koa-app/.npmrc b/dev-packages/e2e-tests/test-applications/node-koa-app/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-koa-app/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/node-koa-app/index.js b/dev-packages/e2e-tests/test-applications/node-koa-app/index.js new file mode 100644 index 000000000000..3ee16ab7200e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-koa-app/index.js @@ -0,0 +1,145 @@ +const Sentry = require('@sentry/node'); + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.E2E_TEST_DSN, + includeLocalVariables: true, + debug: true, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1, + tracePropagationTargets: ['http://localhost:3030', 'external-allowed'], +}); + +const port1 = 3030; +const port2 = 3040; + +const Koa = require('koa'); +const Router = require('@koa/router'); +const http = require('http'); + +const app1 = new Koa(); + +Sentry.setupKoaErrorHandler(app1); + +const router1 = new Router(); + +router1.get('/test-success', ctx => { + ctx.body = { version: 'v1' }; +}); + +router1.get('/test-param/:param', ctx => { + ctx.body = { paramWas: ctx.params.param }; +}); + +router1.get('/test-inbound-headers/:id', ctx => { + const headers = ctx.request.headers; + + ctx.body = { + headers, + id: ctx.params.id, + }; +}); + +router1.get('/test-outgoing-http/:id', async ctx => { + const id = ctx.params.id; + const data = await makeHttpRequest(`http://localhost:3030/test-inbound-headers/${id}`); + + ctx.body = data; +}); + +router1.get('/test-outgoing-fetch/:id', async ctx => { + const id = ctx.params.id; + const response = await fetch(`http://localhost:3030/test-inbound-headers/${id}`); + const data = await response.json(); + + ctx.body = data; +}); + +router1.get('/test-transaction', ctx => { + Sentry.startSpan({ name: 'test-span' }, () => { + Sentry.startSpan({ name: 'child-span' }, () => {}); + }); + + ctx.body = {}; +}); + +router1.get('/test-error', async ctx => { + const exceptionId = Sentry.captureException(new Error('This is an error')); + + await Sentry.flush(2000); + + ctx.body = { exceptionId }; +}); + +router1.get('/test-exception', async ctx => { + throw new Error('This is an exception'); +}); + +router1.get('/test-outgoing-fetch-external-allowed', async ctx => { + const fetchResponse = await fetch(`http://localhost:${port2}/external-allowed`); + const data = await fetchResponse.json(); + + ctx.body = data; +}); + +router1.get('/test-outgoing-fetch-external-disallowed', async ctx => { + const fetchResponse = await fetch(`http://localhost:${port2}/external-disallowed`); + const data = await fetchResponse.json(); + + ctx.body = data; +}); + +router1.get('/test-outgoing-http-external-allowed', async ctx => { + const data = await makeHttpRequest(`http://localhost:${port2}/external-allowed`); + ctx.body = data; +}); + +router1.get('/test-outgoing-http-external-disallowed', async ctx => { + const data = await makeHttpRequest(`http://localhost:${port2}/external-disallowed`); + ctx.body = data; +}); + +app1.use(router1.routes()).use(router1.allowedMethods()); + +app1.listen(port1); + +const app2 = new Koa(); +const router2 = new Router(); + +router2.get('/external-allowed', ctx => { + const headers = ctx.headers; + ctx.body = { headers, route: '/external-allowed' }; +}); + +router2.get('/external-disallowed', ctx => { + const headers = ctx.headers; + ctx.body = { headers, route: '/external-disallowed' }; +}); + +app2.use(router2.routes()).use(router2.allowedMethods()); +app2.listen(port2); + +function makeHttpRequest(url) { + return new Promise(resolve => { + const data = []; + + http + .request(url, httpRes => { + httpRes.on('data', chunk => { + data.push(chunk); + }); + httpRes.on('error', error => { + resolve({ error: error.message, url }); + }); + httpRes.on('end', () => { + 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-koa-app/package.json b/dev-packages/e2e-tests/test-applications/node-koa-app/package.json new file mode 100644 index 000000000000..c9e35dda13ac --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-koa-app/package.json @@ -0,0 +1,28 @@ +{ + "name": "node-koa-app", + "version": "1.0.0", + "private": true, + "scripts": { + "start": "node index.js", + "test": "playwright test", + "clean": "npx rimraf node_modules,pnpm-lock.yaml", + "test:build": "pnpm install", + "test:assert": "pnpm test" + }, + "dependencies": { + "@koa/router": "^12.0.1", + "@sentry/node": "latest || *", + "@sentry/types": "latest || *", + "@types/node": "18.15.1", + "koa": "^2.15.2", + "typescript": "4.9.5" + }, + "devDependencies": { + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", + "@playwright/test": "^1.27.1", + "ts-node": "10.9.1" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-koa-app/playwright.config.ts b/dev-packages/e2e-tests/test-applications/node-koa-app/playwright.config.ts new file mode 100644 index 000000000000..9506d20b6d0c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-koa-app/playwright.config.ts @@ -0,0 +1,77 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; +import { devices } from '@playwright/test'; + +const koaPort = 3030; +const eventProxyPort = 3031; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + testDir: './tests', + /* Maximum time one test can run for. */ + timeout: 150_000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000, + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: 0, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'list', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: `http://localhost:${koaPort}`, + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + // For now we only test Chrome! + // { + // name: 'firefox', + // use: { + // ...devices['Desktop Firefox'], + // }, + // }, + // { + // name: 'webkit', + // use: { + // ...devices['Desktop Safari'], + // }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: [ + { + command: 'pnpm ts-node-script start-event-proxy.ts', + port: eventProxyPort, + }, + { + command: 'pnpm start', + port: koaPort, + }, + ], +}; + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/node-koa-app/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/node-koa-app/start-event-proxy.ts new file mode 100644 index 000000000000..65eda84d3f8a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-koa-app/start-event-proxy.ts @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'node-koa-app', +}); diff --git a/dev-packages/e2e-tests/test-applications/node-koa-app/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-koa-app/tests/errors.test.ts new file mode 100644 index 000000000000..1d6cf604f176 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-koa-app/tests/errors.test.ts @@ -0,0 +1,72 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/event-proxy-server'; +import axios, { AxiosError } from 'axios'; + +const authToken = process.env.E2E_TEST_AUTH_TOKEN; +const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; +const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT; +const EVENT_POLLING_TIMEOUT = 90_000; + +test('Sends exception to Sentry', async ({ baseURL }) => { + const { data } = await axios.get(`${baseURL}/test-error`); + const { exceptionId } = data; + + const url = `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionId}/`; + + console.log(`Polling for error eventId: ${exceptionId}`); + + await expect + .poll( + async () => { + try { + const response = await axios.get(url, { headers: { Authorization: `Bearer ${authToken}` } }); + + return response.status; + } catch (e) { + if (e instanceof AxiosError && e.response) { + if (e.response.status !== 404) { + throw e; + } else { + return e.response.status; + } + } else { + throw e; + } + } + }, + { timeout: EVENT_POLLING_TIMEOUT }, + ) + .toBe(200); +}); + +test('Sends correct error event', async ({ baseURL }) => { + const errorEventPromise = waitForError('node-koa-app', event => { + return !event.type && event.exception?.values?.[0]?.value === 'This is an exception'; + }); + + try { + await axios.get(`${baseURL}/test-exception`); + } catch { + // this results in an error, but we don't care - we want to check the error event + } + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception'); + + expect(errorEvent.request).toEqual({ + method: 'GET', + cookies: {}, + headers: expect.any(Object), + url: 'http://localhost:3030/test-exception', + }); + + expect(errorEvent.transaction).toEqual('GET /test-exception'); + + expect(errorEvent.contexts?.trace).toEqual({ + trace_id: expect.any(String), + span_id: expect.any(String), + parent_span_id: expect.any(String), + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-koa-app/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-koa-app/tests/propagation.test.ts new file mode 100644 index 000000000000..4ed65dbe69a8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-koa-app/tests/propagation.test.ts @@ -0,0 +1,351 @@ +import crypto from 'crypto'; +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; +import { SpanJSON } from '@sentry/types'; +import axios from 'axios'; + +test('Propagates trace for outgoing http requests', async ({ baseURL }) => { + const id = crypto.randomUUID(); + + const inboundTransactionPromise = waitForTransaction('node-koa-app', transactionEvent => { + return ( + transactionEvent.contexts?.trace?.op === 'http.server' && + transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-inbound-headers/${id}` + ); + }); + + const outboundTransactionPromise = waitForTransaction('node-koa-app', transactionEvent => { + return ( + 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/${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 SpanJSON | undefined; + + expect(outgoingHttpSpan).toBeDefined(); + + const outgoingHttpSpanId = outgoingHttpSpan?.span_id; + + expect(traceId).toEqual(expect.any(String)); + + // data is passed through from the inbound request, to verify we have the correct headers set + const inboundHeaderSentryTrace = data.headers?.['sentry-trace']; + const inboundHeaderBaggage = data.headers?.['baggage']; + + expect(inboundHeaderSentryTrace).toEqual(`${traceId}-${outgoingHttpSpanId}-1`); + expect(inboundHeaderBaggage).toBeDefined(); + + const baggage = (inboundHeaderBaggage || '').split(','); + expect(baggage).toEqual( + expect.arrayContaining([ + 'sentry-environment=qa', + `sentry-trace_id=${traceId}`, + expect.stringMatching(/sentry-public_key=/), + ]), + ); + + expect(outboundTransaction.contexts?.trace).toEqual({ + data: { + 'sentry.source': 'route', + 'sentry.origin': 'auto.http.otel.http', + 'sentry.op': 'http.server', + 'sentry.sample_rate': 1, + url: `http://localhost:3030/test-outgoing-http/${id}`, + 'otel.kind': 'SERVER', + 'http.response.status_code': 200, + '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/${id}`, + 'http.user_agent': 'axios/1.6.7', + 'http.flavor': '1.1', + 'net.transport': 'ip_tcp', + 'net.host.ip': expect.any(String), + 'net.host.port': expect.any(Number), + 'net.peer.ip': expect.any(String), + 'net.peer.port': expect.any(Number), + 'http.status_code': 200, + 'http.status_text': 'OK', + 'http.route': '/test-outgoing-http/:id', + }, + op: 'http.server', + span_id: expect.any(String), + status: 'ok', + trace_id: traceId, + origin: 'auto.http.otel.http', + }); + + expect(inboundTransaction.contexts?.trace).toEqual({ + data: { + 'sentry.source': 'route', + 'sentry.origin': 'auto.http.otel.http', + 'sentry.op': 'http.server', + 'sentry.sample_rate': 1, + url: `http://localhost:3030/test-inbound-headers/${id}`, + 'otel.kind': 'SERVER', + 'http.response.status_code': 200, + '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/${id}`, + 'http.flavor': '1.1', + 'net.transport': 'ip_tcp', + 'net.host.ip': expect.any(String), + 'net.host.port': expect.any(Number), + 'net.peer.ip': expect.any(String), + 'net.peer.port': expect.any(Number), + 'http.status_code': 200, + 'http.status_text': 'OK', + 'http.route': '/test-inbound-headers/:id', + }, + op: 'http.server', + parent_span_id: outgoingHttpSpanId, + span_id: expect.any(String), + status: 'ok', + trace_id: traceId, + origin: 'auto.http.otel.http', + }); +}); + +test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { + const id = crypto.randomUUID(); + + const inboundTransactionPromise = waitForTransaction('node-koa-app', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-inbound-headers/${id}` + ); + }); + + const outboundTransactionPromise = waitForTransaction('node-koa-app', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-fetch/${id}` + ); + }); + + 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 SpanJSON | undefined; + + expect(outgoingHttpSpan).toBeDefined(); + + const outgoingHttpSpanId = outgoingHttpSpan?.span_id; + + expect(traceId).toEqual(expect.any(String)); + + // data is passed through from the inbound request, to verify we have the correct headers set + const inboundHeaderSentryTrace = data.headers?.['sentry-trace']; + const inboundHeaderBaggage = data.headers?.['baggage']; + + expect(inboundHeaderSentryTrace).toEqual(`${traceId}-${outgoingHttpSpanId}-1`); + expect(inboundHeaderBaggage).toBeDefined(); + + const baggage = (inboundHeaderBaggage || '').split(','); + expect(baggage).toEqual( + expect.arrayContaining([ + 'sentry-environment=qa', + `sentry-trace_id=${traceId}`, + expect.stringMatching(/sentry-public_key=/), + ]), + ); + + expect(outboundTransaction.contexts?.trace).toEqual({ + data: { + 'sentry.source': 'route', + 'sentry.origin': 'auto.http.otel.http', + 'sentry.op': 'http.server', + 'sentry.sample_rate': 1, + url: `http://localhost:3030/test-outgoing-fetch/${id}`, + 'otel.kind': 'SERVER', + 'http.response.status_code': 200, + '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/${id}`, + 'http.user_agent': 'axios/1.6.7', + 'http.flavor': '1.1', + 'net.transport': 'ip_tcp', + 'net.host.ip': expect.any(String), + 'net.host.port': expect.any(Number), + 'net.peer.ip': expect.any(String), + 'net.peer.port': expect.any(Number), + 'http.status_code': 200, + 'http.status_text': 'OK', + 'http.route': '/test-outgoing-fetch/:id', + }, + op: 'http.server', + span_id: expect.any(String), + status: 'ok', + trace_id: traceId, + origin: 'auto.http.otel.http', + }); + + expect(inboundTransaction.contexts?.trace).toEqual({ + data: expect.objectContaining({ + 'sentry.source': 'route', + 'sentry.origin': 'auto.http.otel.http', + 'sentry.op': 'http.server', + 'sentry.sample_rate': 1, + url: `http://localhost:3030/test-inbound-headers/${id}`, + 'otel.kind': 'SERVER', + 'http.response.status_code': 200, + '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/${id}`, + 'http.flavor': '1.1', + 'net.transport': 'ip_tcp', + 'net.host.ip': expect.any(String), + 'net.host.port': expect.any(Number), + 'net.peer.ip': expect.any(String), + 'net.peer.port': expect.any(Number), + 'http.status_code': 200, + 'http.status_text': 'OK', + 'http.route': '/test-inbound-headers/:id', + }), + op: 'http.server', + parent_span_id: outgoingHttpSpanId, + span_id: expect.any(String), + status: 'ok', + trace_id: traceId, + origin: 'auto.http.otel.http', + }); +}); + +test('Propagates trace for outgoing external http requests', async ({ baseURL }) => { + const inboundTransactionPromise = waitForTransaction('node-koa-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-koa-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-koa-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-koa-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/e2e-tests/test-applications/node-koa-app/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-koa-app/tests/transactions.test.ts new file mode 100644 index 000000000000..1ff7c3f78d6e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-koa-app/tests/transactions.test.ts @@ -0,0 +1,156 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; +import axios, { AxiosError } from 'axios'; + +const authToken = process.env.E2E_TEST_AUTH_TOKEN; +const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; +const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT; +const EVENT_POLLING_TIMEOUT = 90_000; + +test('Sends an API route transaction', async ({ baseURL }) => { + const pageloadTransactionEventPromise = waitForTransaction('node-koa-app', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-transaction' + ); + }); + + await axios.get(`${baseURL}/test-transaction`); + + const transactionEvent = await pageloadTransactionEventPromise; + const transactionEventId = transactionEvent.event_id; + + expect(transactionEvent.contexts?.trace).toEqual({ + data: { + 'sentry.source': 'route', + 'sentry.origin': 'auto.http.otel.http', + 'sentry.op': 'http.server', + 'sentry.sample_rate': 1, + url: 'http://localhost:3030/test-transaction', + 'otel.kind': 'SERVER', + 'http.response.status_code': 200, + 'http.url': 'http://localhost:3030/test-transaction', + 'http.host': 'localhost:3030', + 'net.host.name': 'localhost', + 'http.method': 'GET', + 'http.scheme': 'http', + 'http.target': '/test-transaction', + 'http.user_agent': 'axios/1.6.7', + 'http.flavor': '1.1', + 'net.transport': 'ip_tcp', + 'net.host.ip': expect.any(String), + 'net.host.port': expect.any(Number), + 'net.peer.ip': expect.any(String), + 'net.peer.port': expect.any(Number), + 'http.status_code': 200, + 'http.status_text': 'OK', + 'http.route': '/test-transaction', + }, + op: 'http.server', + span_id: expect.any(String), + status: 'ok', + trace_id: expect.any(String), + origin: 'auto.http.otel.http', + }); + + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: [ + { + data: { + 'koa.name': '', + 'koa.type': 'middleware', + 'otel.kind': 'INTERNAL', + 'sentry.origin': 'manual', + }, + origin: 'manual', + description: 'middleware - ', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + }, + { + data: { + 'http.route': '/test-transaction', + 'koa.name': '/test-transaction', + 'koa.type': 'router', + 'otel.kind': 'INTERNAL', + 'sentry.origin': 'manual', + }, + description: 'router - /test-transaction', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + origin: 'manual', + }, + { + data: { + 'otel.kind': 'INTERNAL', + 'sentry.origin': 'manual', + }, + description: 'test-span', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + origin: 'manual', + }, + { + data: { + 'otel.kind': 'INTERNAL', + 'sentry.origin': 'manual', + }, + description: 'child-span', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + origin: 'manual', + }, + ], + transaction: 'GET /test-transaction', + type: 'transaction', + transaction_info: { + source: 'route', + }, + }), + ); + + await expect + .poll( + async () => { + try { + const response = await axios.get( + `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, + { headers: { Authorization: `Bearer ${authToken}` } }, + ); + + return response.status; + } catch (e) { + if (e instanceof AxiosError && e.response) { + if (e.response.status !== 404) { + throw e; + } else { + return e.response.status; + } + } else { + throw e; + } + } + }, + { + timeout: EVENT_POLLING_TIMEOUT, + }, + ) + .toBe(200); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-koa-app/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-koa-app/tsconfig.json new file mode 100644 index 000000000000..17bd2c1f4c00 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-koa-app/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "types": ["node"], + "esModuleInterop": true, + "lib": ["dom", "dom.iterable", "esnext"], + "strict": true, + "outDir": "dist" + }, + "include": ["*.ts"] +} diff --git a/dev-packages/e2e-tests/test-applications/node-koa-app/yarn.lock b/dev-packages/e2e-tests/test-applications/node-koa-app/yarn.lock new file mode 100644 index 000000000000..201176ad5cdf --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-koa-app/yarn.lock @@ -0,0 +1,312 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@koa/router@^12.0.1": + version "12.0.1" + resolved "https://registry.yarnpkg.com/@koa/router/-/router-12.0.1.tgz#1a66f92a630c02832cf5bbf0db06c9e53e423468" + integrity sha512-ribfPYfHb+Uw3b27Eiw6NPqjhIhTpVFzEWLwyc/1Xp+DCdwRRyIlAUODX+9bPARF6aQtUu1+/PHzdNvRzcs/+Q== + dependencies: + debug "^4.3.4" + http-errors "^2.0.0" + koa-compose "^4.1.0" + methods "^1.1.2" + path-to-regexp "^6.2.1" + +accepts@^1.3.5: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +cache-content-type@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-content-type/-/cache-content-type-1.0.1.tgz#035cde2b08ee2129f4a8315ea8f00a00dba1453c" + integrity sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA== + dependencies: + mime-types "^2.1.18" + ylru "^1.2.0" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== + +content-disposition@~0.5.2: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookies@~0.9.0: + version "0.9.1" + resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.9.1.tgz#3ffed6f60bb4fb5f146feeedba50acc418af67e3" + integrity sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw== + dependencies: + depd "~2.0.0" + keygrip "~1.1.0" + +debug@^4.3.2, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +deep-equal@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" + integrity sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw== + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== + +depd@2.0.0, depd@^2.0.0, depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +destroy@^1.0.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +encodeurl@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +escape-html@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +fresh@~0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +http-assert@^1.3.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.5.0.tgz#c389ccd87ac16ed2dfa6246fd73b926aa00e6b8f" + integrity sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w== + dependencies: + deep-equal "~1.0.1" + http-errors "~1.8.0" + +http-errors@^1.6.3, http-errors@~1.8.0: + version "1.8.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c" + integrity sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.1" + +http-errors@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-generator-function@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" + integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== + dependencies: + has-tostringtag "^1.0.0" + +keygrip@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226" + integrity sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ== + dependencies: + tsscmp "1.0.6" + +koa-compose@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877" + integrity sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw== + +koa-convert@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-2.0.0.tgz#86a0c44d81d40551bae22fee6709904573eea4f5" + integrity sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA== + dependencies: + co "^4.6.0" + koa-compose "^4.1.0" + +koa@^2.15.2: + version "2.15.2" + resolved "https://registry.yarnpkg.com/koa/-/koa-2.15.2.tgz#1e4afe1482d01bd24ed6e30f630a960411f5ebf2" + integrity sha512-MXTeZH3M6AJ8ukW2QZ8wqO3Dcdfh2WRRmjCBkEP+NhKNCiqlO5RDqHmSnsyNrbRJrdjyvIGSJho4vQiWgQJSVA== + dependencies: + accepts "^1.3.5" + cache-content-type "^1.0.0" + content-disposition "~0.5.2" + content-type "^1.0.4" + cookies "~0.9.0" + debug "^4.3.2" + delegates "^1.0.0" + depd "^2.0.0" + destroy "^1.0.4" + encodeurl "^1.0.2" + escape-html "^1.0.3" + fresh "~0.5.2" + http-assert "^1.3.0" + http-errors "^1.6.3" + is-generator-function "^1.0.7" + koa-compose "^4.1.0" + koa-convert "^2.0.0" + on-finished "^2.3.0" + only "~0.0.2" + parseurl "^1.3.2" + statuses "^1.5.0" + type-is "^1.6.16" + vary "^1.1.2" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +methods@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.18, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +on-finished@^2.3.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +only@~0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" + integrity sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ== + +parseurl@^1.3.2: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-to-regexp@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.1.tgz#d54934d6798eb9e5ef14e7af7962c945906918e5" + integrity sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw== + +safe-buffer@5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +"statuses@>= 1.5.0 < 2", statuses@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +tsscmp@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb" + integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA== + +type-is@^1.6.16: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +vary@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +ylru@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ylru/-/ylru-1.4.0.tgz#0cf0aa57e9c24f8a2cbde0cc1ca2c9592ac4e0f6" + integrity sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA== diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-app/.gitignore b/dev-packages/e2e-tests/test-applications/node-nestjs-app/.gitignore new file mode 100644 index 000000000000..4b56acfbebf4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-app/.gitignore @@ -0,0 +1,56 @@ +# compiled output +/dist +/node_modules +/build + +# Logs +logs +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# OS +.DS_Store + +# Tests +/coverage +/.nyc_output + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# temp directory +.temp +.tmp + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-app/.npmrc b/dev-packages/e2e-tests/test-applications/node-nestjs-app/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-app/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-app/nest-cli.json b/dev-packages/e2e-tests/test-applications/node-nestjs-app/nest-cli.json new file mode 100644 index 000000000000..f9aa683b1ad5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-app/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-app/package.json b/dev-packages/e2e-tests/test-applications/node-nestjs-app/package.json new file mode 100644 index 000000000000..6135e17ad1a1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-app/package.json @@ -0,0 +1,48 @@ +{ + "name": "node-nestjs-app", + "version": "0.0.1", + "private": true, + "scripts": { + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "clean": "npx rimraf node_modules,pnpm-lock.yaml", + "test": "playwright test", + "test:build": "pnpm install", + "test:assert": "pnpm test" + }, + "dependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "@sentry/node": "latest || *", + "@sentry/types": "latest || *", + "reflect-metadata": "^0.2.0", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", + "@nestjs/cli": "^10.0.0", + "@nestjs/schematics": "^10.0.0", + "@nestjs/testing": "^10.0.0", + "@playwright/test": "^1.27.1", + "@types/express": "^4.17.17", + "@types/node": "18.15.1", + "@types/supertest": "^6.0.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "eslint": "^8.42.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", + "prettier": "^3.0.0", + "source-map-support": "^0.5.21", + "supertest": "^6.3.3", + "ts-loader": "^9.4.3", + "ts-node": "^10.9.1", + "tsconfig-paths": "^4.2.0", + "typescript": "^4.9.5" + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-app/playwright.config.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-app/playwright.config.ts new file mode 100644 index 000000000000..bb16d8c803f0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-app/playwright.config.ts @@ -0,0 +1,77 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; +import { devices } from '@playwright/test'; + +const nestjsPort = 3030; +const eventProxyPort = 3031; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + testDir: './tests', + /* Maximum time one test can run for. */ + timeout: 150_000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000, + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: 0, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'list', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: `http://localhost:${nestjsPort}`, + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + // For now we only test Chrome! + // { + // name: 'firefox', + // use: { + // ...devices['Desktop Firefox'], + // }, + // }, + // { + // name: 'webkit', + // use: { + // ...devices['Desktop Safari'], + // }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: [ + { + command: 'pnpm ts-node-script start-event-proxy.ts', + port: eventProxyPort, + }, + { + command: 'pnpm start', + port: nestjsPort, + }, + ], +}; + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-app/src/app.controller.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-app/src/app.controller.ts new file mode 100644 index 000000000000..5dda4845d392 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-app/src/app.controller.ts @@ -0,0 +1,82 @@ +import { Controller, Get, Headers, Param } from '@nestjs/common'; +import { AppService1, AppService2 } from './app.service'; + +@Controller() +export class AppController1 { + constructor(private readonly appService: AppService1) {} + + @Get('test-success') + testSuccess() { + return this.appService.testSuccess(); + } + + @Get('test-param/:param') + testParam(@Param() params) { + return this.appService.testParam(params.param); + } + + @Get('test-inbound-headers/:id') + testInboundHeaders(@Headers() headers, @Param('id') id: string) { + return this.appService.testInboundHeaders(headers, id); + } + + @Get('test-outgoing-http/:id') + async testOutgoingHttp(@Param('id') id: string) { + return this.appService.testOutgoingHttp(id); + } + + @Get('test-outgoing-fetch/:id') + async testOutgoingFetch(@Param('id') id: string) { + return this.appService.testOutgoingFetch(id); + } + + @Get('test-transaction') + testTransaction() { + return this.appService.testTransaction(); + } + + @Get('test-error') + async testError() { + return this.appService.testError(); + } + + @Get('test-exception') + async testException() { + return this.appService.testException(); + } + + @Get('test-outgoing-fetch-external-allowed') + async testOutgoingFetchExternalAllowed() { + return this.appService.testOutgoingFetchExternalAllowed(); + } + + @Get('test-outgoing-fetch-external-disallowed') + async testOutgoingFetchExternalDisallowed() { + return this.appService.testOutgoingFetchExternalDisallowed(); + } + + @Get('test-outgoing-http-external-allowed') + async testOutgoingHttpExternalAllowed() { + return this.appService.testOutgoingHttpExternalAllowed(); + } + + @Get('test-outgoing-http-external-disallowed') + async testOutgoingHttpExternalDisallowed() { + return this.appService.testOutgoingHttpExternalDisallowed(); + } +} + +@Controller() +export class AppController2 { + constructor(private readonly appService: AppService2) {} + + @Get('external-allowed') + externalAllowed(@Headers() headers) { + return this.appService.externalAllowed(headers); + } + + @Get('external-disallowed') + externalDisallowed(@Headers() headers) { + return this.appService.externalDisallowed(headers); + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-app/src/app.module.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-app/src/app.module.ts new file mode 100644 index 000000000000..5fda2f1e209f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-app/src/app.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { AppController1, AppController2 } from './app.controller'; +import { AppService1, AppService2 } from './app.service'; + +@Module({ + imports: [], + controllers: [AppController1], + providers: [AppService1], +}) +export class AppModule1 {} + +@Module({ + imports: [], + controllers: [AppController2], + providers: [AppService2], +}) +export class AppModule2 {} diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-app/src/app.service.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-app/src/app.service.ts new file mode 100644 index 000000000000..387668889c24 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-app/src/app.service.ts @@ -0,0 +1,91 @@ +import { Injectable } from '@nestjs/common'; +import * as Sentry from '@sentry/node'; +import { makeHttpRequest } from './utils'; + +@Injectable() +export class AppService1 { + testSuccess() { + return { version: 'v1' }; + } + + testParam(id: string) { + return { + paramWas: id, + }; + } + + testInboundHeaders(headers: Record, id: string) { + return { + headers, + id, + }; + } + + async testOutgoingHttp(id: string) { + const data = await makeHttpRequest(`http://localhost:3030/test-inbound-headers/${id}`); + + return data; + } + + async testOutgoingFetch(id: string) { + const response = await fetch(`http://localhost:3030/test-inbound-headers/${id}`); + const data = await response.json(); + + return data; + } + + testTransaction() { + Sentry.startSpan({ name: 'test-span' }, () => { + Sentry.startSpan({ name: 'child-span' }, () => {}); + }); + } + + async testError() { + const exceptionId = Sentry.captureException(new Error('This is an error')); + + await Sentry.flush(2000); + + return { exceptionId }; + } + + testException() { + throw new Error('This is an exception'); + } + + async testOutgoingFetchExternalAllowed() { + const fetchResponse = await fetch('http://localhost:3040/external-allowed'); + + return fetchResponse.json(); + } + + async testOutgoingFetchExternalDisallowed() { + const fetchResponse = await fetch('http://localhost:3040/external-disallowed'); + + return fetchResponse.json(); + } + + async testOutgoingHttpExternalAllowed() { + return makeHttpRequest('http://localhost:3040/external-allowed'); + } + + async testOutgoingHttpExternalDisallowed() { + return makeHttpRequest('http://localhost:3040/external-disallowed'); + } +} + +@Injectable() +export class AppService2 { + externalAllowed(headers: Record) { + return { + headers, + route: 'external-allowed', + }; + } + + externalDisallowed(headers: Record) { + return { + headers, + route: 'external-disallowed', + }; + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-app/src/main.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-app/src/main.ts new file mode 100644 index 000000000000..f852b29c8e06 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-app/src/main.ts @@ -0,0 +1,26 @@ +import { NestFactory } from '@nestjs/core'; +import * as Sentry from '@sentry/node'; +import { AppModule1, AppModule2 } from './app.module'; + +const app1Port = 3030; +const app2Port = 3040; + +async function bootstrap() { + Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.E2E_TEST_DSN, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1, + tracePropagationTargets: ['http://localhost:3030', '/external-allowed'], + }); + + const app1 = await NestFactory.create(AppModule1); + Sentry.setupNestErrorHandler(app1); + + await app1.listen(app1Port); + + const app2 = await NestFactory.create(AppModule2); + await app2.listen(app2Port); +} + +bootstrap(); diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-app/src/utils.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-app/src/utils.ts new file mode 100644 index 000000000000..27639ef26349 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-app/src/utils.ts @@ -0,0 +1,26 @@ +import * as http from 'http'; + +export function makeHttpRequest(url) { + return new Promise(resolve => { + const data = []; + + http + .request(url, httpRes => { + httpRes.on('data', chunk => { + data.push(chunk); + }); + httpRes.on('error', error => { + resolve({ error: error.message, url }); + }); + httpRes.on('end', () => { + 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-nestjs-app/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-app/start-event-proxy.ts new file mode 100644 index 000000000000..9f99b9ac5d23 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-app/start-event-proxy.ts @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'node-nestjs-app', +}); diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-app/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-app/tests/errors.test.ts new file mode 100644 index 000000000000..8d478c063472 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-app/tests/errors.test.ts @@ -0,0 +1,73 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/event-proxy-server'; +import axios, { AxiosError } from 'axios'; + +const authToken = process.env.E2E_TEST_AUTH_TOKEN; +const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; +const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT; +const EVENT_POLLING_TIMEOUT = 90_000; + +test('Sends captured error to Sentry', async ({ baseURL }) => { + const { data } = await axios.get(`${baseURL}/test-error`); + const { exceptionId } = data; + + const url = `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionId}/`; + + console.log(`Polling for error eventId: ${exceptionId}`); + + await expect + .poll( + async () => { + try { + const response = await axios.get(url, { + headers: { Authorization: `Bearer ${authToken}` }, + }); + + return response.status; + } catch (e) { + if (e instanceof AxiosError && e.response) { + if (e.response.status !== 404) { + throw e; + } else { + return e.response.status; + } + } else { + throw e; + } + } + }, + { timeout: EVENT_POLLING_TIMEOUT }, + ) + .toBe(200); +}); + +test('Sends exception to Sentry', async ({ baseURL }) => { + const errorEventPromise = waitForError('node-nestjs-app', event => { + return !event.type && event.exception?.values?.[0]?.value === 'This is an exception'; + }); + + try { + axios.get(`${baseURL}/test-exception`); + } catch { + // this results in an error, but we don't care - we want to check the error event + } + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception'); + + expect(errorEvent.request).toEqual({ + method: 'GET', + cookies: {}, + headers: expect.any(Object), + url: 'http://localhost:3030/test-exception', + }); + + expect(errorEvent.transaction).toEqual('GET /test-exception'); + + expect(errorEvent.contexts?.trace).toEqual({ + trace_id: expect.any(String), + span_id: expect.any(String), + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-app/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-app/tests/propagation.test.ts new file mode 100644 index 000000000000..698b0833fd0d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-app/tests/propagation.test.ts @@ -0,0 +1,351 @@ +import crypto from 'crypto'; +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; +import { SpanJSON } from '@sentry/types'; +import axios from 'axios'; + +test('Propagates trace for outgoing http requests', async ({ baseURL }) => { + const id = crypto.randomUUID(); + + const inboundTransactionPromise = waitForTransaction('node-nestjs-app', transactionEvent => { + return ( + transactionEvent.contexts?.trace?.op === 'http.server' && + transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-inbound-headers/${id}` + ); + }); + + const outboundTransactionPromise = waitForTransaction('node-nestjs-app', transactionEvent => { + return ( + 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/${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 SpanJSON | undefined; + + expect(outgoingHttpSpan).toBeDefined(); + + const outgoingHttpSpanId = outgoingHttpSpan?.span_id; + + expect(traceId).toEqual(expect.any(String)); + + // data is passed through from the inbound request, to verify we have the correct headers set + const inboundHeaderSentryTrace = data.headers?.['sentry-trace']; + const inboundHeaderBaggage = data.headers?.['baggage']; + + expect(inboundHeaderSentryTrace).toEqual(`${traceId}-${outgoingHttpSpanId}-1`); + expect(inboundHeaderBaggage).toBeDefined(); + + const baggage = (inboundHeaderBaggage || '').split(','); + expect(baggage).toEqual( + expect.arrayContaining([ + 'sentry-environment=qa', + `sentry-trace_id=${traceId}`, + expect.stringMatching(/sentry-public_key=/), + ]), + ); + + expect(outboundTransaction.contexts?.trace).toEqual({ + data: { + 'sentry.source': 'route', + 'sentry.origin': 'auto.http.otel.http', + 'sentry.op': 'http.server', + 'sentry.sample_rate': 1, + url: `http://localhost:3030/test-outgoing-http/${id}`, + 'otel.kind': 'SERVER', + 'http.response.status_code': 200, + '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/${id}`, + 'http.user_agent': 'axios/1.6.7', + 'http.flavor': '1.1', + 'net.transport': 'ip_tcp', + 'net.host.ip': expect.any(String), + 'net.host.port': expect.any(Number), + 'net.peer.ip': expect.any(String), + 'net.peer.port': expect.any(Number), + 'http.status_code': 200, + 'http.status_text': 'OK', + 'http.route': '/test-outgoing-http/:id', + }, + op: 'http.server', + span_id: expect.any(String), + status: 'ok', + trace_id: traceId, + origin: 'auto.http.otel.http', + }); + + expect(inboundTransaction.contexts?.trace).toEqual({ + data: { + 'sentry.source': 'route', + 'sentry.origin': 'auto.http.otel.http', + 'sentry.op': 'http.server', + 'sentry.sample_rate': 1, + url: `http://localhost:3030/test-inbound-headers/${id}`, + 'otel.kind': 'SERVER', + 'http.response.status_code': 200, + '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/${id}`, + 'http.flavor': '1.1', + 'net.transport': 'ip_tcp', + 'net.host.ip': expect.any(String), + 'net.host.port': expect.any(Number), + 'net.peer.ip': expect.any(String), + 'net.peer.port': expect.any(Number), + 'http.status_code': 200, + 'http.status_text': 'OK', + 'http.route': '/test-inbound-headers/:id', + }, + op: 'http.server', + parent_span_id: outgoingHttpSpanId, + span_id: expect.any(String), + status: 'ok', + trace_id: traceId, + origin: 'auto.http.otel.http', + }); +}); + +test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { + const id = crypto.randomUUID(); + + const inboundTransactionPromise = waitForTransaction('node-nestjs-app', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-inbound-headers/${id}` + ); + }); + + const outboundTransactionPromise = waitForTransaction('node-nestjs-app', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-fetch/${id}` + ); + }); + + 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 SpanJSON | undefined; + + expect(outgoingHttpSpan).toBeDefined(); + + const outgoingHttpSpanId = outgoingHttpSpan?.span_id; + + expect(traceId).toEqual(expect.any(String)); + + // data is passed through from the inbound request, to verify we have the correct headers set + const inboundHeaderSentryTrace = data.headers?.['sentry-trace']; + const inboundHeaderBaggage = data.headers?.['baggage']; + + expect(inboundHeaderSentryTrace).toEqual(`${traceId}-${outgoingHttpSpanId}-1`); + expect(inboundHeaderBaggage).toBeDefined(); + + const baggage = (inboundHeaderBaggage || '').split(','); + expect(baggage).toEqual( + expect.arrayContaining([ + 'sentry-environment=qa', + `sentry-trace_id=${traceId}`, + expect.stringMatching(/sentry-public_key=/), + ]), + ); + + expect(outboundTransaction.contexts?.trace).toEqual({ + data: { + 'sentry.source': 'route', + 'sentry.origin': 'auto.http.otel.http', + 'sentry.op': 'http.server', + 'sentry.sample_rate': 1, + url: `http://localhost:3030/test-outgoing-fetch/${id}`, + 'otel.kind': 'SERVER', + 'http.response.status_code': 200, + '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/${id}`, + 'http.user_agent': 'axios/1.6.7', + 'http.flavor': '1.1', + 'net.transport': 'ip_tcp', + 'net.host.ip': expect.any(String), + 'net.host.port': expect.any(Number), + 'net.peer.ip': expect.any(String), + 'net.peer.port': expect.any(Number), + 'http.status_code': 200, + 'http.status_text': 'OK', + 'http.route': '/test-outgoing-fetch/:id', + }, + op: 'http.server', + span_id: expect.any(String), + status: 'ok', + trace_id: traceId, + origin: 'auto.http.otel.http', + }); + + expect(inboundTransaction.contexts?.trace).toEqual({ + data: expect.objectContaining({ + 'sentry.source': 'route', + 'sentry.origin': 'auto.http.otel.http', + 'sentry.op': 'http.server', + 'sentry.sample_rate': 1, + url: `http://localhost:3030/test-inbound-headers/${id}`, + 'otel.kind': 'SERVER', + 'http.response.status_code': 200, + '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/${id}`, + 'http.flavor': '1.1', + 'net.transport': 'ip_tcp', + 'net.host.ip': expect.any(String), + 'net.host.port': expect.any(Number), + 'net.peer.ip': expect.any(String), + 'net.peer.port': expect.any(Number), + 'http.status_code': 200, + 'http.status_text': 'OK', + 'http.route': '/test-inbound-headers/:id', + }), + op: 'http.server', + parent_span_id: outgoingHttpSpanId, + span_id: expect.any(String), + status: 'ok', + trace_id: traceId, + origin: 'auto.http.otel.http', + }); +}); + +test('Propagates trace for outgoing external http requests', async ({ baseURL }) => { + const inboundTransactionPromise = waitForTransaction('node-nestjs-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({ + headers: expect.objectContaining({ + 'sentry-trace': `${traceId}-${spanId}-1`, + baggage: expect.any(String), + }), + route: 'external-allowed', + }); + + 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-nestjs-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-nestjs-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({ + headers: expect.objectContaining({ + 'sentry-trace': `${traceId}-${spanId}-1`, + baggage: expect.any(String), + }), + route: 'external-allowed', + }); + + 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-nestjs-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/e2e-tests/test-applications/node-nestjs-app/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-app/tests/transactions.test.ts new file mode 100644 index 000000000000..e5a78a6f6fb7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-app/tests/transactions.test.ts @@ -0,0 +1,140 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; +import axios, { AxiosError } from 'axios'; + +const authToken = process.env.E2E_TEST_AUTH_TOKEN; +const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; +const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT; +const EVENT_POLLING_TIMEOUT = 90_000; + +test('Sends an API route transaction', async ({ baseURL }) => { + const pageloadTransactionEventPromise = waitForTransaction('node-nestjs-app', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-transaction' + ); + }); + + await axios.get(`${baseURL}/test-transaction`); + + const transactionEvent = await pageloadTransactionEventPromise; + const transactionEventId = transactionEvent.event_id; + + expect(transactionEvent.contexts?.trace).toEqual({ + data: { + 'sentry.source': 'route', + 'sentry.origin': 'auto.http.otel.http', + 'sentry.op': 'http.server', + 'sentry.sample_rate': 1, + url: 'http://localhost:3030/test-transaction', + 'otel.kind': 'SERVER', + 'http.response.status_code': 200, + 'http.url': 'http://localhost:3030/test-transaction', + 'http.host': 'localhost:3030', + 'net.host.name': 'localhost', + 'http.method': 'GET', + 'http.scheme': 'http', + 'http.target': '/test-transaction', + 'http.user_agent': 'axios/1.6.7', + 'http.flavor': '1.1', + 'net.transport': 'ip_tcp', + 'net.host.ip': expect.any(String), + 'net.host.port': expect.any(Number), + 'net.peer.ip': expect.any(String), + 'net.peer.port': expect.any(Number), + 'http.status_code': 200, + 'http.status_text': 'OK', + 'http.route': '/test-transaction', + }, + op: 'http.server', + span_id: expect.any(String), + status: 'ok', + trace_id: expect.any(String), + origin: 'auto.http.otel.http', + }); + + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + data: { + 'express.name': '/test-transaction', + 'express.type': 'request_handler', + 'http.route': '/test-transaction', + 'otel.kind': 'INTERNAL', + 'sentry.origin': 'auto.http.otel.express', + }, + description: 'request handler - /test-transaction', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + origin: 'auto.http.otel.express', + }, + { + data: { + 'otel.kind': 'INTERNAL', + 'sentry.origin': 'manual', + }, + description: 'test-span', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + origin: 'manual', + }, + { + data: { + 'otel.kind': 'INTERNAL', + 'sentry.origin': 'manual', + }, + description: 'child-span', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + origin: 'manual', + }, + ]), + transaction: 'GET /test-transaction', + type: 'transaction', + transaction_info: { + source: 'route', + }, + }), + ); + + await expect + .poll( + async () => { + try { + const response = await axios.get( + `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, + { headers: { Authorization: `Bearer ${authToken}` } }, + ); + + return response.status; + } catch (e) { + if (e instanceof AxiosError && e.response) { + if (e.response.status !== 404) { + throw e; + } else { + return e.response.status; + } + } else { + throw e; + } + } + }, + { + timeout: EVENT_POLLING_TIMEOUT, + }, + ) + .toBe(200); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-app/tsconfig.build.json b/dev-packages/e2e-tests/test-applications/node-nestjs-app/tsconfig.build.json new file mode 100644 index 000000000000..26c30d4eddf2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-app/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist"] +} diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-app/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-nestjs-app/tsconfig.json new file mode 100644 index 000000000000..95f5641cf7f3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-app/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": false, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false + } +} diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/event-proxy-server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/event-proxy-server.ts deleted file mode 100644 index d14ca5cb5e72..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/event-proxy-server.ts +++ /dev/null @@ -1,253 +0,0 @@ -import * as fs from 'fs'; -import * as http from 'http'; -import * as https from 'https'; -import type { AddressInfo } from 'net'; -import * as os from 'os'; -import * as path from 'path'; -import * as util from 'util'; -import * as zlib from 'zlib'; -import type { Envelope, EnvelopeItem, Event } from '@sentry/types'; -import { parseEnvelope } from '@sentry/utils'; - -const readFile = util.promisify(fs.readFile); -const writeFile = util.promisify(fs.writeFile); - -interface EventProxyServerOptions { - /** Port to start the event proxy server at. */ - port: number; - /** The name for the proxy server used for referencing it with listener functions */ - proxyServerName: string; -} - -interface SentryRequestCallbackData { - envelope: Envelope; - rawProxyRequestBody: string; - rawSentryResponseBody: string; - sentryResponseStatusCode?: number; -} - -/** - * Starts an event proxy server that will proxy events to sentry when the `tunnel` option is used. Point the `tunnel` - * option to this server (like this `tunnel: http://localhost:${port option}/`). - */ -export async function startEventProxyServer(options: EventProxyServerOptions): Promise { - const eventCallbackListeners: Set<(data: string) => void> = new Set(); - - const proxyServer = http.createServer((proxyRequest, proxyResponse) => { - const proxyRequestChunks: Uint8Array[] = []; - - proxyRequest.addListener('data', (chunk: Buffer) => { - proxyRequestChunks.push(chunk); - }); - - proxyRequest.addListener('error', err => { - throw err; - }); - - proxyRequest.addListener('end', () => { - const proxyRequestBody = - proxyRequest.headers['content-encoding'] === 'gzip' - ? zlib.gunzipSync(Buffer.concat(proxyRequestChunks)).toString() - : Buffer.concat(proxyRequestChunks).toString(); - - let envelopeHeader = JSON.parse(proxyRequestBody.split('\n')[0]); - - if (!envelopeHeader.dsn) { - throw new Error('[event-proxy-server] No dsn on envelope header. Please set tunnel option.'); - } - - const { origin, pathname, host } = new URL(envelopeHeader.dsn); - - const projectId = pathname.substring(1); - const sentryIngestUrl = `${origin}/api/${projectId}/envelope/`; - - proxyRequest.headers.host = host; - - const sentryResponseChunks: Uint8Array[] = []; - - const sentryRequest = https.request( - sentryIngestUrl, - { headers: proxyRequest.headers, method: proxyRequest.method }, - sentryResponse => { - sentryResponse.addListener('data', (chunk: Buffer) => { - proxyResponse.write(chunk, 'binary'); - sentryResponseChunks.push(chunk); - }); - - sentryResponse.addListener('end', () => { - eventCallbackListeners.forEach(listener => { - const rawSentryResponseBody = Buffer.concat(sentryResponseChunks).toString(); - - const data: SentryRequestCallbackData = { - envelope: parseEnvelope(proxyRequestBody), - rawProxyRequestBody: proxyRequestBody, - rawSentryResponseBody, - sentryResponseStatusCode: sentryResponse.statusCode, - }; - - listener(Buffer.from(JSON.stringify(data)).toString('base64')); - }); - proxyResponse.end(); - }); - - sentryResponse.addListener('error', err => { - throw err; - }); - - proxyResponse.writeHead(sentryResponse.statusCode || 500, sentryResponse.headers); - }, - ); - - sentryRequest.write(Buffer.concat(proxyRequestChunks), 'binary'); - sentryRequest.end(); - }); - }); - - const proxyServerStartupPromise = new Promise(resolve => { - proxyServer.listen(options.port, () => { - resolve(); - }); - }); - - const eventCallbackServer = http.createServer((eventCallbackRequest, eventCallbackResponse) => { - eventCallbackResponse.statusCode = 200; - eventCallbackResponse.setHeader('connection', 'keep-alive'); - - const callbackListener = (data: string): void => { - eventCallbackResponse.write(data.concat('\n'), 'utf8'); - }; - - eventCallbackListeners.add(callbackListener); - - eventCallbackRequest.on('close', () => { - eventCallbackListeners.delete(callbackListener); - }); - - eventCallbackRequest.on('error', () => { - eventCallbackListeners.delete(callbackListener); - }); - }); - - const eventCallbackServerStartupPromise = new Promise(resolve => { - eventCallbackServer.listen(0, () => { - const port = String((eventCallbackServer.address() as AddressInfo).port); - void registerCallbackServerPort(options.proxyServerName, port).then(resolve); - }); - }); - - await eventCallbackServerStartupPromise; - await proxyServerStartupPromise; - return; -} - -export async function waitForRequest( - proxyServerName: string, - callback: (eventData: SentryRequestCallbackData) => Promise | boolean, -): Promise { - const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName); - - return new Promise((resolve, reject) => { - const request = http.request(`http://localhost:${eventCallbackServerPort}/`, {}, response => { - let eventContents = ''; - - response.on('error', err => { - reject(err); - }); - - response.on('data', (chunk: Buffer) => { - const chunkString = chunk.toString('utf8'); - chunkString.split('').forEach(char => { - if (char === '\n') { - const eventCallbackData: SentryRequestCallbackData = JSON.parse( - Buffer.from(eventContents, 'base64').toString('utf8'), - ); - const callbackResult = callback(eventCallbackData); - if (typeof callbackResult !== 'boolean') { - callbackResult.then( - match => { - if (match) { - response.destroy(); - resolve(eventCallbackData); - } - }, - err => { - throw err; - }, - ); - } else if (callbackResult) { - response.destroy(); - resolve(eventCallbackData); - } - eventContents = ''; - } else { - eventContents = eventContents.concat(char); - } - }); - }); - }); - - request.end(); - }); -} - -export function waitForEnvelopeItem( - proxyServerName: string, - callback: (envelopeItem: EnvelopeItem) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForRequest(proxyServerName, async eventData => { - const envelopeItems = eventData.envelope[1]; - for (const envelopeItem of envelopeItems) { - if (await callback(envelopeItem)) { - resolve(envelopeItem); - return true; - } - } - return false; - }).catch(reject); - }); -} - -export function waitForError( - proxyServerName: string, - callback: (transactionEvent: Event) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForEnvelopeItem(proxyServerName, async envelopeItem => { - const [envelopeItemHeader, envelopeItemBody] = envelopeItem; - if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) { - resolve(envelopeItemBody as Event); - return true; - } - return false; - }).catch(reject); - }); -} - -export function waitForTransaction( - proxyServerName: string, - callback: (transactionEvent: Event) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForEnvelopeItem(proxyServerName, async envelopeItem => { - const [envelopeItemHeader, envelopeItemBody] = envelopeItem; - if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) { - resolve(envelopeItemBody as Event); - return true; - } - return false; - }).catch(reject); - }); -} - -const TEMP_FILE_PREFIX = 'event-proxy-server-'; - -async function registerCallbackServerPort(serverName: string, port: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - await writeFile(tmpFilePath, port, { encoding: 'utf8' }); -} - -function retrieveCallbackServerPort(serverName: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - return readFile(tmpFilePath, 'utf8'); -} diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json index deac95bf05c0..42ac39f210a9 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json @@ -18,6 +18,7 @@ "@sentry/sveltekit": "latest || *" }, "devDependencies": { + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", "@playwright/test": "^1.36.2", "@sentry/types": "latest || *", "@sentry/utils": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/+page.svelte index d8175182884a..71a0ce872185 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/+page.svelte +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/+page.svelte @@ -29,4 +29,10 @@
  • Route with nested fetch in server load
  • +
  • + Nav 1 +
  • +
  • + Nav 2 +
  • diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/nav1/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/nav1/+page.svelte new file mode 100644 index 000000000000..31abffc512a2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/nav1/+page.svelte @@ -0,0 +1 @@ +

    Navigation 1

    diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/nav2/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/nav2/+page.svelte new file mode 100644 index 000000000000..20b44bb32da9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/nav2/+page.svelte @@ -0,0 +1 @@ +

    Navigation 2

    diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/start-event-proxy.ts index 3af64eb5960a..fcf2c0b9addc 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/start-event-proxy.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/start-event-proxy.ts @@ -1,4 +1,4 @@ -import { startEventProxyServer } from './event-proxy-server'; +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; startEventProxyServer({ port: 3031, diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/errors.client.test.ts index 329ac33b7880..eb83566a475d 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/errors.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/errors.client.test.ts @@ -1,10 +1,10 @@ import { expect, test } from '@playwright/test'; -import { waitForError } from '../event-proxy-server'; +import { waitForError } from '@sentry-internal/event-proxy-server'; import { waitForInitialPageload } from './utils'; test.describe('client-side errors', () => { test('captures error thrown on click', async ({ page }) => { - await page.goto('/client-error'); + await waitForInitialPageload(page, { route: '/client-error' }); const errorEventPromise = waitForError('sveltekit-2', errorEvent => { return errorEvent?.exception?.values?.[0]?.value === 'Click Error'; @@ -27,6 +27,8 @@ test.describe('client-side errors', () => { ); expect(errorEvent.tags).toMatchObject({ runtime: 'browser' }); + + expect(errorEvent.transaction).toEqual('/client-error'); }); test('captures universal load error', async ({ page }) => { @@ -52,5 +54,6 @@ test.describe('client-side errors', () => { ); expect(errorEvent.tags).toMatchObject({ runtime: 'browser' }); + expect(errorEvent.transaction).toEqual('/universal-load-error'); }); }); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/errors.server.test.ts index 5240489b0934..ffdfad2932c8 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/errors.server.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError } from '../event-proxy-server'; +import { waitForError } from '@sentry-internal/event-proxy-server'; test.describe('server-side errors', () => { test('captures universal load error', async ({ page }) => { diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.client.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.client.test.ts new file mode 100644 index 000000000000..11e647ff07ff --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.client.test.ts @@ -0,0 +1,69 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; +import { waitForInitialPageload } from './utils'; + +test.describe('client-specific performance events', () => { + test('multiple navigations have distinct traces', async ({ page }) => { + const navigationTxn1EventPromise = waitForTransaction('sveltekit-2', txnEvent => { + return txnEvent?.transaction === '/nav1' && txnEvent.contexts?.trace?.op === 'navigation'; + }); + + const navigationTxn2EventPromise = waitForTransaction('sveltekit-2', txnEvent => { + return txnEvent?.transaction === '/' && txnEvent.contexts?.trace?.op === 'navigation'; + }); + + const navigationTxn3EventPromise = waitForTransaction('sveltekit-2', txnEvent => { + return txnEvent?.transaction === '/nav2' && txnEvent.contexts?.trace?.op === 'navigation'; + }); + + await waitForInitialPageload(page); + + const [navigationTxn1Event] = await Promise.all([navigationTxn1EventPromise, page.getByText('Nav 1').click()]); + const [navigationTxn2Event] = await Promise.all([navigationTxn2EventPromise, page.goBack()]); + const [navigationTxn3Event] = await Promise.all([navigationTxn3EventPromise, page.getByText('Nav 2').click()]); + + expect(navigationTxn1Event).toMatchObject({ + transaction: '/nav1', + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.sveltekit', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }, + }, + }); + + expect(navigationTxn2Event).toMatchObject({ + transaction: '/', + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.sveltekit', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }, + }, + }); + + expect(navigationTxn3Event).toMatchObject({ + transaction: '/nav2', + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.sveltekit', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }, + }, + }); + + // traces should NOT be connected + expect(navigationTxn1Event.contexts?.trace?.trace_id).not.toBe(navigationTxn2Event.contexts?.trace?.trace_id); + expect(navigationTxn2Event.contexts?.trace?.trace_id).not.toBe(navigationTxn3Event.contexts?.trace?.trace_id); + expect(navigationTxn1Event.contexts?.trace?.trace_id).not.toBe(navigationTxn3Event.contexts?.trace?.trace_id); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.server.test.ts index 7aec23b30d7a..e04a056ca875 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.server.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '../event-proxy-server'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; test('server pageload request span has nested request span for sub request', async ({ page }) => { const serverTxnEventPromise = waitForTransaction('sveltekit-2', txnEvent => { diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.test.ts index 53b1ea128f00..adfeb4e31d85 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.test.ts @@ -1,11 +1,9 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '../event-proxy-server'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; import { waitForInitialPageload } from './utils'; test.describe('performance events', () => { test('capture a distributed pageload trace', async ({ page }) => { - await page.goto('/users/123xyz'); - const clientTxnEventPromise = waitForTransaction('sveltekit-2', txnEvent => { return txnEvent?.transaction === '/users/[id]'; }); @@ -14,7 +12,8 @@ test.describe('performance events', () => { return txnEvent?.transaction === 'GET /users/[id]'; }); - const [clientTxnEvent, serverTxnEvent, _] = await Promise.all([ + const [_, clientTxnEvent, serverTxnEvent] = await Promise.all([ + page.goto('/users/123xyz'), clientTxnEventPromise, serverTxnEventPromise, expect(page.getByText('User id: 123xyz')).toBeVisible(), @@ -56,8 +55,6 @@ test.describe('performance events', () => { }); test('capture a distributed navigation trace', async ({ page }) => { - await waitForInitialPageload(page); - const clientNavigationTxnEventPromise = waitForTransaction('sveltekit-2', txnEvent => { return txnEvent?.transaction === '/users' && txnEvent.contexts?.trace?.op === 'navigation'; }); @@ -66,6 +63,8 @@ test.describe('performance events', () => { return txnEvent?.transaction === 'GET /users'; }); + await waitForInitialPageload(page); + // navigation to page const clickPromise = page.getByText('Route with Server Load').click(); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/utils.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/utils.ts index b48b949abdd5..2fa35d9ae874 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/utils.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/utils.ts @@ -1,5 +1,5 @@ import { Page } from '@playwright/test'; -import { waitForTransaction } from '../event-proxy-server'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; /** * Helper function that waits for the initial pageload to complete. @@ -18,7 +18,7 @@ import { waitForTransaction } from '../event-proxy-server'; */ export async function waitForInitialPageload( page: Page, - opts?: { route?: string; parameterizedRoute?: string; debug: boolean }, + opts?: { route?: string; parameterizedRoute?: string; debug?: boolean }, ) { const route = opts?.route ?? '/'; const txnName = opts?.parameterizedRoute ?? route; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/event-proxy-server.ts b/dev-packages/e2e-tests/test-applications/sveltekit/event-proxy-server.ts deleted file mode 100644 index eba9ffef8682..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/event-proxy-server.ts +++ /dev/null @@ -1,253 +0,0 @@ -import * as fs from 'fs'; -import * as http from 'http'; -import * as https from 'https'; -import type { AddressInfo } from 'net'; -import * as os from 'os'; -import * as path from 'path'; -import * as util from 'util'; -import * as zlib from 'zlib'; -import type { Envelope, EnvelopeItem, Event } from '@sentry/types'; -import { parseEnvelope } from '@sentry/utils'; - -const readFile = util.promisify(fs.readFile); -const writeFile = util.promisify(fs.writeFile); - -interface EventProxyServerOptions { - /** Port to start the event proxy server at. */ - port: number; - /** The name for the proxy server used for referencing it with listener functions */ - proxyServerName: string; -} - -interface SentryRequestCallbackData { - envelope: Envelope; - rawProxyRequestBody: string; - rawSentryResponseBody: string; - sentryResponseStatusCode?: number; -} - -/** - * Starts an event proxy server that will proxy events to sentry when the `tunnel` option is used. Point the `tunnel` - * option to this server (like this `tunnel: http://localhost:${port option}/`). - */ -export async function startEventProxyServer(options: EventProxyServerOptions): Promise { - const eventCallbackListeners: Set<(data: string) => void> = new Set(); - - const proxyServer = http.createServer((proxyRequest, proxyResponse) => { - const proxyRequestChunks: Uint8Array[] = []; - - proxyRequest.addListener('data', (chunk: Buffer) => { - proxyRequestChunks.push(chunk); - }); - - proxyRequest.addListener('error', err => { - throw err; - }); - - proxyRequest.addListener('end', () => { - const proxyRequestBody = - proxyRequest.headers['content-encoding'] === 'gzip' - ? zlib.gunzipSync(Buffer.concat(proxyRequestChunks)).toString() - : Buffer.concat(proxyRequestChunks).toString(); - - let envelopeHeader = JSON.parse(proxyRequestBody.split('\n')[0]); - - if (!envelopeHeader.dsn) { - throw new Error('[event-proxy-server] No dsn on envelope header. Please set tunnel option.'); - } - - const { origin, pathname, host } = new URL(envelopeHeader.dsn); - - const projectId = pathname.substring(1); - const sentryIngestUrl = `${origin}/api/${projectId}/envelope/`; - - proxyRequest.headers.host = host; - - const sentryResponseChunks: Uint8Array[] = []; - - const sentryRequest = https.request( - sentryIngestUrl, - { headers: proxyRequest.headers, method: proxyRequest.method }, - sentryResponse => { - sentryResponse.addListener('data', (chunk: Buffer) => { - proxyResponse.write(chunk, 'binary'); - sentryResponseChunks.push(chunk); - }); - - sentryResponse.addListener('end', () => { - eventCallbackListeners.forEach(listener => { - const rawSentryResponseBody = Buffer.concat(sentryResponseChunks).toString(); - - const data: SentryRequestCallbackData = { - envelope: parseEnvelope(proxyRequestBody), - rawProxyRequestBody: proxyRequestBody, - rawSentryResponseBody, - sentryResponseStatusCode: sentryResponse.statusCode, - }; - - listener(Buffer.from(JSON.stringify(data)).toString('base64')); - }); - proxyResponse.end(); - }); - - sentryResponse.addListener('error', err => { - throw err; - }); - - proxyResponse.writeHead(sentryResponse.statusCode || 500, sentryResponse.headers); - }, - ); - - sentryRequest.write(Buffer.concat(proxyRequestChunks), 'binary'); - sentryRequest.end(); - }); - }); - - const proxyServerStartupPromise = new Promise(resolve => { - proxyServer.listen(options.port, () => { - resolve(); - }); - }); - - const eventCallbackServer = http.createServer((eventCallbackRequest, eventCallbackResponse) => { - eventCallbackResponse.statusCode = 200; - eventCallbackResponse.setHeader('connection', 'keep-alive'); - - const callbackListener = (data: string): void => { - eventCallbackResponse.write(data.concat('\n'), 'utf8'); - }; - - eventCallbackListeners.add(callbackListener); - - eventCallbackRequest.on('close', () => { - eventCallbackListeners.delete(callbackListener); - }); - - eventCallbackRequest.on('error', () => { - eventCallbackListeners.delete(callbackListener); - }); - }); - - const eventCallbackServerStartupPromise = new Promise(resolve => { - eventCallbackServer.listen(0, () => { - const port = String((eventCallbackServer.address() as AddressInfo).port); - void registerCallbackServerPort(options.proxyServerName, port).then(resolve); - }); - }); - - await eventCallbackServerStartupPromise; - await proxyServerStartupPromise; - return; -} - -export async function waitForRequest( - proxyServerName: string, - callback: (eventData: SentryRequestCallbackData) => Promise | boolean, -): Promise { - const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName); - - return new Promise((resolve, reject) => { - const request = http.request(`http://localhost:${eventCallbackServerPort}/`, {}, response => { - let eventContents = ''; - - response.on('error', err => { - reject(err); - }); - - response.on('data', (chunk: Buffer) => { - const chunkString = chunk.toString('utf8'); - chunkString.split('').forEach(char => { - if (char === '\n') { - const eventCallbackData: SentryRequestCallbackData = JSON.parse( - Buffer.from(eventContents, 'base64').toString('utf8'), - ); - const callbackResult = callback(eventCallbackData); - if (typeof callbackResult !== 'boolean') { - callbackResult.then( - match => { - if (match) { - response.destroy(); - resolve(eventCallbackData); - } - }, - err => { - throw err; - }, - ); - } else if (callbackResult) { - response.destroy(); - resolve(eventCallbackData); - } - eventContents = ''; - } else { - eventContents = eventContents.concat(char); - } - }); - }); - }); - - request.end(); - }); -} - -export function waitForEnvelopeItem( - proxyServerName: string, - callback: (envelopeItem: EnvelopeItem) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForRequest(proxyServerName, async eventData => { - const envelopeItems = eventData.envelope[1]; - for (const envelopeItem of envelopeItems) { - if (await callback(envelopeItem)) { - resolve(envelopeItem); - return true; - } - } - return false; - }).catch(reject); - }); -} - -export function waitForError( - proxyServerName: string, - callback: (transactionEvent: Event) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForEnvelopeItem(proxyServerName, async envelopeItem => { - const [envelopeItemHeader, envelopeItemBody] = envelopeItem; - if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) { - resolve(envelopeItemBody as Event); - return true; - } - return false; - }).catch(reject); - }); -} - -export function waitForTransaction( - proxyServerName: string, - callback: (transactionEvent: Event) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForEnvelopeItem(proxyServerName, async envelopeItem => { - const [envelopeItemHeader, envelopeItemBody] = envelopeItem; - if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) { - resolve(envelopeItemBody as Event); - return true; - } - return false; - }).catch(reject); - }); -} - -const TEMP_FILE_PREFIX = 'event-proxy-server-'; - -async function registerCallbackServerPort(serverName: string, port: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - await writeFile(tmpFilePath, port, { encoding: 'utf8' }); -} - -async function retrieveCallbackServerPort(serverName: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - return await readFile(tmpFilePath, 'utf8'); -} diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/package.json b/dev-packages/e2e-tests/test-applications/sveltekit/package.json index 5a6b2d4d083c..1fbbcfbcefac 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit/package.json @@ -17,6 +17,7 @@ "@sentry/sveltekit": "latest || *" }, "devDependencies": { + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", "@playwright/test": "^1.41.1", "@sentry/types": "latest || *", "@sentry/utils": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/sveltekit/start-event-proxy.ts index db7640e0d825..cb0fd75c1530 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/start-event-proxy.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit/start-event-proxy.ts @@ -1,4 +1,4 @@ -import { startEventProxyServer } from './event-proxy-server'; +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; startEventProxyServer({ port: 3031, diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.client.test.ts index ee366338391d..7f0a5c50faa0 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.client.test.ts @@ -1,10 +1,10 @@ import { expect, test } from '@playwright/test'; -import { waitForError } from '../event-proxy-server'; +import { waitForError } from '@sentry-internal/event-proxy-server'; import { waitForInitialPageload } from '../utils'; test.describe('client-side errors', () => { test('captures error thrown on click', async ({ page }) => { - await page.goto('/client-error'); + await waitForInitialPageload(page, { route: '/client-error' }); const errorEventPromise = waitForError('sveltekit', errorEvent => { return errorEvent?.exception?.values?.[0]?.value === 'Click Error'; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.server.test.ts index c36d44c80068..6274239a936b 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.server.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError } from '../event-proxy-server'; +import { waitForError } from '@sentry-internal/event-proxy-server'; test.describe('server-side errors', () => { test('captures universal load error', async ({ page }) => { diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/test/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit/test/performance.server.test.ts index 49fde5f01045..a363d28f291b 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/test/performance.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit/test/performance.server.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '../event-proxy-server'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; test('server pageload request span has nested request span for sub request', async ({ page }) => { const serverTxnEventPromise = waitForTransaction('sveltekit', txnEvent => { diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/test/performance.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit/test/performance.test.ts index 9982678da122..cb2ac4446a49 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/test/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit/test/performance.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '../event-proxy-server.js'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; import { waitForInitialPageload } from '../utils.js'; test('sends a pageload transaction', async ({ page }) => { @@ -26,8 +26,6 @@ test('sends a pageload transaction', async ({ page }) => { }); test('captures a distributed pageload trace', async ({ page }) => { - await page.goto('/users/123xyz'); - const clientTxnEventPromise = waitForTransaction('sveltekit', txnEvent => { return txnEvent?.transaction === '/users/[id]'; }); @@ -36,6 +34,8 @@ test('captures a distributed pageload trace', async ({ page }) => { return txnEvent?.transaction === 'GET /users/[id]'; }); + await page.goto('/users/123xyz'); + const [clientTxnEvent, serverTxnEvent] = await Promise.all([clientTxnEventPromise, serverTxnEventPromise]); expect(clientTxnEvent).toMatchObject({ @@ -71,8 +71,6 @@ test('captures a distributed pageload trace', async ({ page }) => { }); test('captures a distributed navigation trace', async ({ page }) => { - await waitForInitialPageload(page); - const clientNavigationTxnEventPromise = waitForTransaction('sveltekit', txnEvent => { return txnEvent?.transaction === '/users/[id]'; }); @@ -81,6 +79,8 @@ test('captures a distributed navigation trace', async ({ page }) => { return txnEvent?.transaction === 'GET /users/[id]'; }); + await waitForInitialPageload(page); + // navigation to page const clickPromise = page.getByText('Route with Params').click(); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/utils.ts b/dev-packages/e2e-tests/test-applications/sveltekit/utils.ts index 2886873bb8fb..c919c1d72e95 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/utils.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit/utils.ts @@ -1,5 +1,5 @@ import { Page } from '@playwright/test'; -import { waitForTransaction } from './event-proxy-server'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; /** * Helper function that waits for the initial pageload to complete. @@ -18,7 +18,7 @@ import { waitForTransaction } from './event-proxy-server'; */ export async function waitForInitialPageload( page: Page, - opts?: { route?: string; parameterizedRoute?: string; debug: boolean }, + opts?: { route?: string; parameterizedRoute?: string; debug?: boolean }, ) { const route = opts?.route ?? '/'; const txnName = opts?.parameterizedRoute ?? route; diff --git a/dev-packages/e2e-tests/test-applications/vue-3/event-proxy-server.ts b/dev-packages/e2e-tests/test-applications/vue-3/event-proxy-server.ts deleted file mode 100644 index d14ca5cb5e72..000000000000 --- a/dev-packages/e2e-tests/test-applications/vue-3/event-proxy-server.ts +++ /dev/null @@ -1,253 +0,0 @@ -import * as fs from 'fs'; -import * as http from 'http'; -import * as https from 'https'; -import type { AddressInfo } from 'net'; -import * as os from 'os'; -import * as path from 'path'; -import * as util from 'util'; -import * as zlib from 'zlib'; -import type { Envelope, EnvelopeItem, Event } from '@sentry/types'; -import { parseEnvelope } from '@sentry/utils'; - -const readFile = util.promisify(fs.readFile); -const writeFile = util.promisify(fs.writeFile); - -interface EventProxyServerOptions { - /** Port to start the event proxy server at. */ - port: number; - /** The name for the proxy server used for referencing it with listener functions */ - proxyServerName: string; -} - -interface SentryRequestCallbackData { - envelope: Envelope; - rawProxyRequestBody: string; - rawSentryResponseBody: string; - sentryResponseStatusCode?: number; -} - -/** - * Starts an event proxy server that will proxy events to sentry when the `tunnel` option is used. Point the `tunnel` - * option to this server (like this `tunnel: http://localhost:${port option}/`). - */ -export async function startEventProxyServer(options: EventProxyServerOptions): Promise { - const eventCallbackListeners: Set<(data: string) => void> = new Set(); - - const proxyServer = http.createServer((proxyRequest, proxyResponse) => { - const proxyRequestChunks: Uint8Array[] = []; - - proxyRequest.addListener('data', (chunk: Buffer) => { - proxyRequestChunks.push(chunk); - }); - - proxyRequest.addListener('error', err => { - throw err; - }); - - proxyRequest.addListener('end', () => { - const proxyRequestBody = - proxyRequest.headers['content-encoding'] === 'gzip' - ? zlib.gunzipSync(Buffer.concat(proxyRequestChunks)).toString() - : Buffer.concat(proxyRequestChunks).toString(); - - let envelopeHeader = JSON.parse(proxyRequestBody.split('\n')[0]); - - if (!envelopeHeader.dsn) { - throw new Error('[event-proxy-server] No dsn on envelope header. Please set tunnel option.'); - } - - const { origin, pathname, host } = new URL(envelopeHeader.dsn); - - const projectId = pathname.substring(1); - const sentryIngestUrl = `${origin}/api/${projectId}/envelope/`; - - proxyRequest.headers.host = host; - - const sentryResponseChunks: Uint8Array[] = []; - - const sentryRequest = https.request( - sentryIngestUrl, - { headers: proxyRequest.headers, method: proxyRequest.method }, - sentryResponse => { - sentryResponse.addListener('data', (chunk: Buffer) => { - proxyResponse.write(chunk, 'binary'); - sentryResponseChunks.push(chunk); - }); - - sentryResponse.addListener('end', () => { - eventCallbackListeners.forEach(listener => { - const rawSentryResponseBody = Buffer.concat(sentryResponseChunks).toString(); - - const data: SentryRequestCallbackData = { - envelope: parseEnvelope(proxyRequestBody), - rawProxyRequestBody: proxyRequestBody, - rawSentryResponseBody, - sentryResponseStatusCode: sentryResponse.statusCode, - }; - - listener(Buffer.from(JSON.stringify(data)).toString('base64')); - }); - proxyResponse.end(); - }); - - sentryResponse.addListener('error', err => { - throw err; - }); - - proxyResponse.writeHead(sentryResponse.statusCode || 500, sentryResponse.headers); - }, - ); - - sentryRequest.write(Buffer.concat(proxyRequestChunks), 'binary'); - sentryRequest.end(); - }); - }); - - const proxyServerStartupPromise = new Promise(resolve => { - proxyServer.listen(options.port, () => { - resolve(); - }); - }); - - const eventCallbackServer = http.createServer((eventCallbackRequest, eventCallbackResponse) => { - eventCallbackResponse.statusCode = 200; - eventCallbackResponse.setHeader('connection', 'keep-alive'); - - const callbackListener = (data: string): void => { - eventCallbackResponse.write(data.concat('\n'), 'utf8'); - }; - - eventCallbackListeners.add(callbackListener); - - eventCallbackRequest.on('close', () => { - eventCallbackListeners.delete(callbackListener); - }); - - eventCallbackRequest.on('error', () => { - eventCallbackListeners.delete(callbackListener); - }); - }); - - const eventCallbackServerStartupPromise = new Promise(resolve => { - eventCallbackServer.listen(0, () => { - const port = String((eventCallbackServer.address() as AddressInfo).port); - void registerCallbackServerPort(options.proxyServerName, port).then(resolve); - }); - }); - - await eventCallbackServerStartupPromise; - await proxyServerStartupPromise; - return; -} - -export async function waitForRequest( - proxyServerName: string, - callback: (eventData: SentryRequestCallbackData) => Promise | boolean, -): Promise { - const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName); - - return new Promise((resolve, reject) => { - const request = http.request(`http://localhost:${eventCallbackServerPort}/`, {}, response => { - let eventContents = ''; - - response.on('error', err => { - reject(err); - }); - - response.on('data', (chunk: Buffer) => { - const chunkString = chunk.toString('utf8'); - chunkString.split('').forEach(char => { - if (char === '\n') { - const eventCallbackData: SentryRequestCallbackData = JSON.parse( - Buffer.from(eventContents, 'base64').toString('utf8'), - ); - const callbackResult = callback(eventCallbackData); - if (typeof callbackResult !== 'boolean') { - callbackResult.then( - match => { - if (match) { - response.destroy(); - resolve(eventCallbackData); - } - }, - err => { - throw err; - }, - ); - } else if (callbackResult) { - response.destroy(); - resolve(eventCallbackData); - } - eventContents = ''; - } else { - eventContents = eventContents.concat(char); - } - }); - }); - }); - - request.end(); - }); -} - -export function waitForEnvelopeItem( - proxyServerName: string, - callback: (envelopeItem: EnvelopeItem) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForRequest(proxyServerName, async eventData => { - const envelopeItems = eventData.envelope[1]; - for (const envelopeItem of envelopeItems) { - if (await callback(envelopeItem)) { - resolve(envelopeItem); - return true; - } - } - return false; - }).catch(reject); - }); -} - -export function waitForError( - proxyServerName: string, - callback: (transactionEvent: Event) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForEnvelopeItem(proxyServerName, async envelopeItem => { - const [envelopeItemHeader, envelopeItemBody] = envelopeItem; - if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) { - resolve(envelopeItemBody as Event); - return true; - } - return false; - }).catch(reject); - }); -} - -export function waitForTransaction( - proxyServerName: string, - callback: (transactionEvent: Event) => Promise | boolean, -): Promise { - return new Promise((resolve, reject) => { - waitForEnvelopeItem(proxyServerName, async envelopeItem => { - const [envelopeItemHeader, envelopeItemBody] = envelopeItem; - if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) { - resolve(envelopeItemBody as Event); - return true; - } - return false; - }).catch(reject); - }); -} - -const TEMP_FILE_PREFIX = 'event-proxy-server-'; - -async function registerCallbackServerPort(serverName: string, port: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - await writeFile(tmpFilePath, port, { encoding: 'utf8' }); -} - -function retrieveCallbackServerPort(serverName: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - return readFile(tmpFilePath, 'utf8'); -} diff --git a/dev-packages/e2e-tests/test-applications/vue-3/package.json b/dev-packages/e2e-tests/test-applications/vue-3/package.json index 5de1fdc7d2e9..a40000731607 100644 --- a/dev-packages/e2e-tests/test-applications/vue-3/package.json +++ b/dev-packages/e2e-tests/test-applications/vue-3/package.json @@ -20,6 +20,7 @@ "vue-router": "^4.2.5" }, "devDependencies": { + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server/", "@playwright/test": "^1.41.1", "@sentry/types": "latest || *", "@sentry/utils": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/vue-3/playwright.config.ts b/dev-packages/e2e-tests/test-applications/vue-3/playwright.config.ts index 16dd640e58ef..a5ebf85ba99c 100644 --- a/dev-packages/e2e-tests/test-applications/vue-3/playwright.config.ts +++ b/dev-packages/e2e-tests/test-applications/vue-3/playwright.config.ts @@ -65,10 +65,7 @@ const config: PlaywrightTestConfig = { port: eventProxyPort, }, { - command: - testEnv === 'development' - ? `pnpm wait-port ${eventProxyPort} && pnpm preview --port ${vuePort}` - : `pnpm wait-port ${eventProxyPort} && pnpm preview --port ${vuePort}`, + command: `pnpm wait-port ${eventProxyPort} && pnpm preview --port ${vuePort}`, port: vuePort, }, ], diff --git a/dev-packages/e2e-tests/test-applications/vue-3/src/router/index.ts b/dev-packages/e2e-tests/test-applications/vue-3/src/router/index.ts index a17208711eff..8c3ac217716f 100644 --- a/dev-packages/e2e-tests/test-applications/vue-3/src/router/index.ts +++ b/dev-packages/e2e-tests/test-applications/vue-3/src/router/index.ts @@ -17,6 +17,10 @@ const router = createRouter({ path: '/users/:id', component: () => import('../views/UserIdView.vue'), }, + { + path: '/users-error/:id', + component: () => import('../views/UserIdErrorView.vue'), + }, ], }); diff --git a/dev-packages/e2e-tests/test-applications/vue-3/src/views/UserIdErrorView.vue b/dev-packages/e2e-tests/test-applications/vue-3/src/views/UserIdErrorView.vue new file mode 100644 index 000000000000..dab7ea873f37 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/src/views/UserIdErrorView.vue @@ -0,0 +1,10 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/vue-3/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/vue-3/start-event-proxy.ts index 6435984ad069..e8c8fdf3cd46 100644 --- a/dev-packages/e2e-tests/test-applications/vue-3/start-event-proxy.ts +++ b/dev-packages/e2e-tests/test-applications/vue-3/start-event-proxy.ts @@ -1,4 +1,4 @@ -import { startEventProxyServer } from './event-proxy-server'; +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; startEventProxyServer({ port: 3031, diff --git a/dev-packages/e2e-tests/test-applications/vue-3/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/vue-3/tests/errors.test.ts index b9933299c8c0..14ab59ad7570 100644 --- a/dev-packages/e2e-tests/test-applications/vue-3/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/vue-3/tests/errors.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError } from '../event-proxy-server'; +import { waitForError } from '@sentry-internal/event-proxy-server'; test('sends an error', async ({ page }) => { const errorPromise = waitForError('vue-3', async errorEvent => { @@ -25,5 +25,34 @@ test('sends an error', async ({ page }) => { }, ], }, + transaction: '/', + }); +}); + +test('sends an error with a parameterized transaction name', async ({ page }) => { + const errorPromise = waitForError('vue-3', async errorEvent => { + return !errorEvent.type; + }); + + await page.goto(`/users-error/456`); + + await page.locator('#userErrorBtn').click(); + + const error = await errorPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'This is a Vue test error', + mechanism: { + type: 'generic', + handled: false, + }, + }, + ], + }, + transaction: '/users-error/:id', }); }); diff --git a/dev-packages/e2e-tests/test-applications/vue-3/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/vue-3/tests/performance.test.ts index dc5bd500eee3..aded68211784 100644 --- a/dev-packages/e2e-tests/test-applications/vue-3/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/vue-3/tests/performance.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '../event-proxy-server'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; test('sends a pageload transaction with a parameterized URL', async ({ page }) => { const transactionPromise = waitForTransaction('vue-3', async transactionEvent => { diff --git a/dev-packages/e2e-tests/test-applications/webpack-4/.npmrc b/dev-packages/e2e-tests/test-applications/webpack-4/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/webpack-4/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/webpack-4/build.mjs b/dev-packages/e2e-tests/test-applications/webpack-4/build.mjs new file mode 100644 index 000000000000..11874cb62374 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/webpack-4/build.mjs @@ -0,0 +1,44 @@ +import * as path from 'path'; +import * as url from 'url'; +import HtmlWebpackPlugin from 'html-webpack-plugin'; +import TerserPlugin from 'terser-webpack-plugin'; +import webpack from 'webpack'; + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); + +webpack( + { + entry: path.join(__dirname, 'entry.js'), + output: { + path: path.join(__dirname, 'build'), + filename: 'app.js', + }, + optimization: { + minimize: true, + minimizer: [new TerserPlugin()], + }, + plugins: [new HtmlWebpackPlugin(), new webpack.EnvironmentPlugin(['E2E_TEST_DSN'])], + mode: 'production', + }, + (err, stats) => { + if (err) { + console.error(err.stack || err); + if (err.details) { + console.error(err.details); + } + return; + } + + const info = stats.toJson(); + + if (stats.hasErrors()) { + console.error(info.errors); + process.exit(1); + } + + if (stats.hasWarnings()) { + console.warn(info.warnings); + process.exit(1); + } + }, +); diff --git a/dev-packages/e2e-tests/test-applications/webpack-4/entry.js b/dev-packages/e2e-tests/test-applications/webpack-4/entry.js new file mode 100644 index 000000000000..4fd9cd67e7e3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/webpack-4/entry.js @@ -0,0 +1,11 @@ +import { browserTracingIntegration, captureException, init } from '@sentry/browser'; + +init({ + dsn: process.env.E2E_TEST_DSN, + integrations: [browserTracingIntegration()], +}); + +setTimeout(() => { + const eventId = captureException(new Error('I am an error!')); + window.capturedExceptionId = eventId; +}, 2000); diff --git a/dev-packages/e2e-tests/test-applications/webpack-4/package.json b/dev-packages/e2e-tests/test-applications/webpack-4/package.json new file mode 100644 index 000000000000..ee99ff43128e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/webpack-4/package.json @@ -0,0 +1,18 @@ +{ + "name": "webpack-4-test", + "version": "1.0.0", + "scripts": { + "start": "serve -s build", + "build": "node build.mjs", + "test:build": "pnpm install && npx playwright install && pnpm build", + "test:assert": "playwright test" + }, + "devDependencies": { + "@playwright/test": "^1.42.1", + "@sentry/browser": "latest || *", + "webpack": "^4.47.0", + "terser-webpack-plugin": "^4.2.3", + "html-webpack-plugin": "^4.5.2", + "serve": "^14.2.1" + } +} diff --git a/dev-packages/e2e-tests/test-applications/webpack-4/playwright.config.ts b/dev-packages/e2e-tests/test-applications/webpack-4/playwright.config.ts new file mode 100644 index 000000000000..5f93f826ebf0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/webpack-4/playwright.config.ts @@ -0,0 +1,70 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; +import { devices } from '@playwright/test'; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + testDir: './tests', + /* Maximum time one test can run for. */ + timeout: 150_000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000, + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: 0, + /* Opt out of parallel tests on CI. */ + workers: 1, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'list', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + // For now we only test Chrome! + // { + // name: 'firefox', + // use: { + // ...devices['Desktop Firefox'], + // }, + // }, + // { + // name: 'webkit', + // use: { + // ...devices['Desktop Safari'], + // }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'pnpm start', + port: 3030, + env: { + PORT: '3030', + }, + }, +}; + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/webpack-4/tests/behaviour-test.spec.ts b/dev-packages/e2e-tests/test-applications/webpack-4/tests/behaviour-test.spec.ts new file mode 100644 index 000000000000..4f762a4028d4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/webpack-4/tests/behaviour-test.spec.ts @@ -0,0 +1,44 @@ +import { expect, test } from '@playwright/test'; +import axios, { AxiosError } from 'axios'; + +const EVENT_POLLING_TIMEOUT = 90_000; + +const authToken = process.env.E2E_TEST_AUTH_TOKEN; +const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; +const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT; + +test('Sends an exception to Sentry', async ({ page }) => { + await page.goto('/'); + + const exceptionIdHandle = await page.waitForFunction(() => window.capturedExceptionId); + const exceptionEventId = await exceptionIdHandle.jsonValue(); + + console.log(`Polling for error eventId: ${exceptionEventId}`); + + await expect + .poll( + async () => { + try { + const response = await axios.get( + `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionEventId}/`, + { headers: { Authorization: `Bearer ${authToken}` } }, + ); + return response.status; + } catch (e) { + if (e instanceof AxiosError && e.response) { + if (e.response.status !== 404) { + throw e; + } else { + return e.response.status; + } + } else { + throw e; + } + } + }, + { + timeout: EVENT_POLLING_TIMEOUT, + }, + ) + .toBe(200); +}); diff --git a/dev-packages/e2e-tests/verdaccio-config/config.yaml b/dev-packages/e2e-tests/verdaccio-config/config.yaml index 2cb28f875977..8638c7fb170a 100644 --- a/dev-packages/e2e-tests/verdaccio-config/config.yaml +++ b/dev-packages/e2e-tests/verdaccio-config/config.yaml @@ -92,12 +92,6 @@ packages: unpublish: $all # proxy: npmjs # Don't proxy for E2E tests! - '@sentry/node-experimental': - access: $all - publish: $all - unpublish: $all - # proxy: npmjs # Don't proxy for E2E tests! - '@sentry/opentelemetry': access: $all publish: $all diff --git a/dev-packages/event-proxy-server/.eslintrc.js b/dev-packages/event-proxy-server/.eslintrc.js new file mode 100644 index 000000000000..175b9389af00 --- /dev/null +++ b/dev-packages/event-proxy-server/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + env: { + node: true, + }, + extends: ['../../.eslintrc.js'], + overrides: [], +}; diff --git a/dev-packages/event-proxy-server/package.json b/dev-packages/event-proxy-server/package.json new file mode 100644 index 000000000000..78c58b271050 --- /dev/null +++ b/dev-packages/event-proxy-server/package.json @@ -0,0 +1,49 @@ +{ + "private": true, + "version": "8.0.0-alpha.7", + "name": "@sentry-internal/event-proxy-server", + "author": "Sentry", + "license": "MIT", + "main": "build/cjs/index.js", + "module": "build/esm/index.js", + "types": "build/types/index.d.ts", + "files": [ + "cjs", + "esm", + "types", + "types-ts3.8" + ], + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "types": "./build/types/index.d.ts", + "default": "./build/esm/index.js" + }, + "require": { + "types": "./build/types/index.d.ts", + "default": "./build/cjs/index.js" + } + } + }, + "sideEffects": false, + "engines": { + "node": ">=14.18" + }, + "scripts": { + "fix": "eslint . --format stylish --fix", + "lint": "eslint . --format stylish", + "build": "run-s build:transpile build:types", + "build:dev": "yarn build", + "build:transpile": "rollup -c rollup.npm.config.mjs", + "build:types": "tsc -p tsconfig.types.json", + "clean": "rimraf -g ./node_modules ./build" + }, + "dependencies": { + "@sentry/types": "8.0.0-alpha.7", + "@sentry/utils": "8.0.0-alpha.7" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/packages/tracing-internal/rollup.npm.config.mjs b/dev-packages/event-proxy-server/rollup.npm.config.mjs similarity index 79% rename from packages/tracing-internal/rollup.npm.config.mjs rename to dev-packages/event-proxy-server/rollup.npm.config.mjs index fd61fbf7c62c..b684e2efe16b 100644 --- a/packages/tracing-internal/rollup.npm.config.mjs +++ b/dev-packages/event-proxy-server/rollup.npm.config.mjs @@ -6,7 +6,6 @@ export default makeNPMConfigVariants( output: { // set exports to 'named' or 'auto' so that rollup doesn't warn exports: 'named', - // set preserveModules to false because we want to bundle everything into one file. preserveModules: false, }, }, diff --git a/dev-packages/e2e-tests/test-applications/angular-17/event-proxy-server.ts b/dev-packages/event-proxy-server/src/event-proxy-server.ts similarity index 95% rename from dev-packages/e2e-tests/test-applications/angular-17/event-proxy-server.ts rename to dev-packages/event-proxy-server/src/event-proxy-server.ts index d14ca5cb5e72..36be080fb097 100644 --- a/dev-packages/e2e-tests/test-applications/angular-17/event-proxy-server.ts +++ b/dev-packages/event-proxy-server/src/event-proxy-server.ts @@ -50,13 +50,13 @@ export async function startEventProxyServer(options: EventProxyServerOptions): P ? zlib.gunzipSync(Buffer.concat(proxyRequestChunks)).toString() : Buffer.concat(proxyRequestChunks).toString(); - let envelopeHeader = JSON.parse(proxyRequestBody.split('\n')[0]); + const envelopeHeader: EnvelopeItem[0] = JSON.parse(proxyRequestBody.split('\n')[0]); if (!envelopeHeader.dsn) { throw new Error('[event-proxy-server] No dsn on envelope header. Please set tunnel option.'); } - const { origin, pathname, host } = new URL(envelopeHeader.dsn); + const { origin, pathname, host } = new URL(envelopeHeader.dsn as string); const projectId = pathname.substring(1); const sentryIngestUrl = `${origin}/api/${projectId}/envelope/`; @@ -131,6 +131,7 @@ export async function startEventProxyServer(options: EventProxyServerOptions): P const eventCallbackServerStartupPromise = new Promise(resolve => { eventCallbackServer.listen(0, () => { const port = String((eventCallbackServer.address() as AddressInfo).port); + // eslint-disable-next-line @typescript-eslint/no-floating-promises void registerCallbackServerPort(options.proxyServerName, port).then(resolve); }); }); @@ -140,6 +141,7 @@ export async function startEventProxyServer(options: EventProxyServerOptions): P return; } +/** Wait for a request to be sent. */ export async function waitForRequest( proxyServerName: string, callback: (eventData: SentryRequestCallbackData) => Promise | boolean, @@ -190,6 +192,7 @@ export async function waitForRequest( }); } +/** Wait for a specific envelope item to be sent. */ export function waitForEnvelopeItem( proxyServerName: string, callback: (envelopeItem: EnvelopeItem) => Promise | boolean, @@ -208,6 +211,7 @@ export function waitForEnvelopeItem( }); } +/** Wait for an error to be sent. */ export function waitForError( proxyServerName: string, callback: (transactionEvent: Event) => Promise | boolean, @@ -224,6 +228,7 @@ export function waitForError( }); } +/** Wait for a transaction to be sent. */ export function waitForTransaction( proxyServerName: string, callback: (transactionEvent: Event) => Promise | boolean, diff --git a/dev-packages/event-proxy-server/src/index.ts b/dev-packages/event-proxy-server/src/index.ts new file mode 100644 index 000000000000..9ee4dfc54520 --- /dev/null +++ b/dev-packages/event-proxy-server/src/index.ts @@ -0,0 +1,7 @@ +export { + startEventProxyServer, + waitForEnvelopeItem, + waitForError, + waitForRequest, + waitForTransaction, +} from './event-proxy-server'; diff --git a/dev-packages/event-proxy-server/tsconfig.json b/dev-packages/event-proxy-server/tsconfig.json new file mode 100644 index 000000000000..825380109ceb --- /dev/null +++ b/dev-packages/event-proxy-server/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": {}, + "include": ["src/**/*.ts"] +} diff --git a/packages/node-experimental/tsconfig.types.json b/dev-packages/event-proxy-server/tsconfig.types.json similarity index 100% rename from packages/node-experimental/tsconfig.types.json rename to dev-packages/event-proxy-server/tsconfig.types.json diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 224182a428f6..c5fed2a1e07f 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -16,18 +16,17 @@ "build:types": "tsc -p tsconfig.types.json", "clean": "rimraf -g **/node_modules && run-p clean:script", "clean:script": "node scripts/clean.js", - "prisma:init": "(cd suites/tracing-experimental/prisma-orm && ts-node ./setup.ts)", + "prisma:init": "(cd suites/tracing/prisma-orm && ts-node ./setup.ts)", "lint": "eslint . --format stylish", "fix": "eslint . --format stylish --fix", "type-check": "tsc", "pretest": "run-s --silent prisma:init", - "test": "ts-node ./utils/run-tests.ts", - "jest": "jest --config ./jest.config.js", + "test": "jest --config ./jest.config.js", "test:watch": "yarn test --watch" }, "dependencies": { "@hapi/hapi": "^20.3.0", - "@nestjs/common": "^10.3.3", + "@nestjs/common": "^10.3.7", "@nestjs/core": "^10.3.3", "@nestjs/platform-express": "^10.3.3", "@prisma/client": "5.9.1", diff --git a/dev-packages/node-integration-tests/suites/express/handle-error-scope-data-loss/server.ts b/dev-packages/node-integration-tests/suites/express/handle-error-scope-data-loss/server.ts index ad45cd5d6713..079d9834b01c 100644 --- a/dev-packages/node-integration-tests/suites/express/handle-error-scope-data-loss/server.ts +++ b/dev-packages/node-integration-tests/suites/express/handle-error-scope-data-loss/server.ts @@ -1,8 +1,5 @@ -import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; -import * as Sentry from '@sentry/node-experimental'; -import express from 'express'; - -const app = express(); +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', @@ -10,7 +7,10 @@ Sentry.init({ transport: loggingTransport, }); -app.use(Sentry.Handlers.requestHandler()); +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import express from 'express'; + +const app = express(); Sentry.setTag('global', 'tag'); @@ -26,6 +26,6 @@ app.get('/test/isolationScope', () => { throw new Error('isolation_test_error'); }); -app.use(Sentry.Handlers.errorHandler()); +Sentry.setupExpressErrorHandler(app); startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/handle-error/server.ts b/dev-packages/node-integration-tests/suites/express/handle-error/server.ts index da163f524b87..1f452fbecc97 100644 --- a/dev-packages/node-integration-tests/suites/express/handle-error/server.ts +++ b/dev-packages/node-integration-tests/suites/express/handle-error/server.ts @@ -1,8 +1,5 @@ -import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; -import * as Sentry from '@sentry/node-experimental'; -import express from 'express'; - -const app = express(); +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', @@ -10,12 +7,15 @@ Sentry.init({ transport: loggingTransport, }); -app.use(Sentry.Handlers.requestHandler()); +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import express from 'express'; + +const app = express(); app.get('/test/express', () => { throw new Error('test_error'); }); -app.use(Sentry.Handlers.errorHandler()); +Sentry.setupExpressErrorHandler(app); startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-infix-parameterized/server.ts b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-infix-parameterized/server.ts index daac56d420e1..c41ce7e3ae1a 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-infix-parameterized/server.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-infix-parameterized/server.ts @@ -1,20 +1,21 @@ -import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; -import * as Sentry from '@sentry/node-experimental'; -import cors from 'cors'; -import express from 'express'; - -const app = express(); +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', - integrations: [Sentry.httpIntegration({ tracing: true }), new Sentry.Integrations.Express({ app })], + integrations: [ + // TODO: This used to have the Express integration + ], tracesSampleRate: 1.0, transport: loggingTransport, }); -app.use(Sentry.Handlers.requestHandler()); -app.use(Sentry.Handlers.tracingHandler()); +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import cors from 'cors'; +import express from 'express'; + +const app = express(); app.use(cors()); @@ -30,6 +31,6 @@ const root = express.Router(); app.use('/api2/v1', root); app.use('/api/v1', APIv1); -app.use(Sentry.Handlers.errorHandler()); +Sentry.setupExpressErrorHandler(app); startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-infix/server.ts b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-infix/server.ts index 7b9b9981d03d..0e97e7fe4718 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-infix/server.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-infix/server.ts @@ -1,20 +1,21 @@ -import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; -import * as Sentry from '@sentry/node-experimental'; -import cors from 'cors'; -import express from 'express'; - -const app = express(); +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', - integrations: [Sentry.httpIntegration({ tracing: true }), new Sentry.Integrations.Express({ app })], + integrations: [ + // TODO: This used to have the Express integration + ], tracesSampleRate: 1.0, transport: loggingTransport, }); -app.use(Sentry.Handlers.requestHandler()); -app.use(Sentry.Handlers.tracingHandler()); +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import cors from 'cors'; +import express from 'express'; + +const app = express(); app.use(cors()); @@ -30,6 +31,6 @@ const root = express.Router(); app.use('/api/v1', root); app.use('/api2/v1', APIv1); -app.use(Sentry.Handlers.errorHandler()); +Sentry.setupExpressErrorHandler(app); startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-parameterized-reverse/server.ts b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-parameterized-reverse/server.ts index 93bd6040c8c0..31f41de294d5 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-parameterized-reverse/server.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-parameterized-reverse/server.ts @@ -1,20 +1,21 @@ -import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; -import * as Sentry from '@sentry/node-experimental'; -import cors from 'cors'; -import express from 'express'; - -const app = express(); +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', - integrations: [Sentry.httpIntegration({ tracing: true }), new Sentry.Integrations.Express({ app })], + integrations: [ + // TODO: This used to have the Express integration + ], tracesSampleRate: 1.0, transport: loggingTransport, }); -app.use(Sentry.Handlers.requestHandler()); -app.use(Sentry.Handlers.tracingHandler()); +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import cors from 'cors'; +import express from 'express'; + +const app = express(); app.use(cors()); @@ -30,6 +31,6 @@ const root = express.Router(); app.use('/api/v1', APIv1); app.use('/api', root); -app.use(Sentry.Handlers.errorHandler()); +Sentry.setupExpressErrorHandler(app); startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-parameterized/server.ts b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-parameterized/server.ts index 70579abb1b5b..f24e8754cb89 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-parameterized/server.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-parameterized/server.ts @@ -1,20 +1,21 @@ -import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; -import * as Sentry from '@sentry/node-experimental'; -import cors from 'cors'; -import express from 'express'; - -const app = express(); +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', - integrations: [Sentry.httpIntegration({ tracing: true }), new Sentry.Integrations.Express({ app })], + integrations: [ + // TODO: This used to use the Express integration + ], tracesSampleRate: 1.0, transport: loggingTransport, }); -app.use(Sentry.Handlers.requestHandler()); -app.use(Sentry.Handlers.tracingHandler()); +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import cors from 'cors'; +import express from 'express'; + +const app = express(); app.use(cors()); @@ -30,6 +31,6 @@ const root = express.Router(); app.use('/api', root); app.use('/api/v1', APIv1); -app.use(Sentry.Handlers.errorHandler()); +Sentry.setupExpressErrorHandler(app); startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-same-length-parameterized copy/server.ts b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-same-length-parameterized copy/server.ts index e601c325ef02..a006358edc25 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-same-length-parameterized copy/server.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-same-length-parameterized copy/server.ts @@ -1,20 +1,21 @@ -import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; -import * as Sentry from '@sentry/node-experimental'; -import cors from 'cors'; -import express from 'express'; - -const app = express(); +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', - integrations: [Sentry.httpIntegration({ tracing: true }), new Sentry.Integrations.Express({ app })], + integrations: [ + // TODO: This used to have the Express integration + ], tracesSampleRate: 1.0, transport: loggingTransport, }); -app.use(Sentry.Handlers.requestHandler()); -app.use(Sentry.Handlers.tracingHandler()); +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import cors from 'cors'; +import express from 'express'; + +const app = express(); app.use(cors()); @@ -30,6 +31,6 @@ const root = express.Router(); app.use('/api/v1', APIv1); app.use('/api', root); -app.use(Sentry.Handlers.errorHandler()); +Sentry.setupExpressErrorHandler(app); startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-same-length-parameterized/server.ts b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-same-length-parameterized/server.ts index eecaef18bfcc..a85ef02682d6 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-same-length-parameterized/server.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-same-length-parameterized/server.ts @@ -1,20 +1,21 @@ -import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; -import * as Sentry from '@sentry/node-experimental'; -import cors from 'cors'; -import express from 'express'; - -const app = express(); +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', - integrations: [Sentry.httpIntegration({ tracing: true }), new Sentry.Integrations.Express({ app })], + integrations: [ + // TODO: This used to have the Express integration + ], tracesSampleRate: 1.0, transport: loggingTransport, }); -app.use(Sentry.Handlers.requestHandler()); -app.use(Sentry.Handlers.tracingHandler()); +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import cors from 'cors'; +import express from 'express'; + +const app = express(); app.use(cors()); @@ -30,6 +31,6 @@ const root = express.Router(); app.use('/api', root); app.use('/api/v1', APIv1); -app.use(Sentry.Handlers.errorHandler()); +Sentry.setupExpressErrorHandler(app); startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix/server.ts b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix/server.ts index b4a7b184f8e7..4c03905d5d2a 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix/server.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix/server.ts @@ -1,20 +1,21 @@ -import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; -import * as Sentry from '@sentry/node-experimental'; -import cors from 'cors'; -import express from 'express'; - -const app = express(); +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', - integrations: [Sentry.httpIntegration({ tracing: true }), new Sentry.Integrations.Express({ app })], + integrations: [ + // TODO: This used to have the Express integration + ], tracesSampleRate: 1.0, transport: loggingTransport, }); -app.use(Sentry.Handlers.requestHandler()); -app.use(Sentry.Handlers.tracingHandler()); +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import cors from 'cors'; +import express from 'express'; + +const app = express(); app.use(cors()); @@ -30,6 +31,6 @@ const root = express.Router(); app.use('/api', root); app.use('/api/v1', APIv1); -app.use(Sentry.Handlers.errorHandler()); +Sentry.setupExpressErrorHandler(app); startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/complex-router/server.ts b/dev-packages/node-integration-tests/suites/express/multiple-routers/complex-router/server.ts index 32257b000481..bdc8c03d176e 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/complex-router/server.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/complex-router/server.ts @@ -1,19 +1,20 @@ -import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; -import * as Sentry from '@sentry/node-experimental'; -import express from 'express'; - -const app = express(); +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', - integrations: [Sentry.httpIntegration({ tracing: true }), new Sentry.Integrations.Express({ app })], + integrations: [ + // TODO: This used to use the Express integration + ], tracesSampleRate: 1.0, transport: loggingTransport, }); -app.use(Sentry.Handlers.requestHandler()); -app.use(Sentry.Handlers.tracingHandler()); +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import express from 'express'; + +const app = express(); const APIv1 = express.Router(); @@ -30,6 +31,6 @@ const router = express.Router(); app.use('/api', router); app.use('/api/api/v1', APIv1.use('/sub-router', APIv1)); -app.use(Sentry.Handlers.errorHandler()); +Sentry.setupExpressErrorHandler(app); startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/complex-router/test.ts b/dev-packages/node-integration-tests/suites/express/multiple-routers/complex-router/test.ts index a72b2743e1d4..d3791083f1f1 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/complex-router/test.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/complex-router/test.ts @@ -1,84 +1,88 @@ +import { conditionalTest } from '../../../../utils'; import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; afterAll(() => { cleanupChildProcesses(); }); -test('should construct correct url with multiple parameterized routers, when param is also contain in middle layer route and express used multiple middlewares with route', done => { - // parse node.js major version - const [major] = process.versions.node.split('.').map(Number); - // Split test result base on major node version because regex d flag is support from node 16+ +// Before Node 16, parametrization is not working properly here +conditionalTest({ min: 16 })('complex-router', () => { + test('should construct correct url with multiple parameterized routers, when param is also contain in middle layer route and express used multiple middlewares with route', done => { + // parse node.js major version + const [major] = process.versions.node.split('.').map(Number); + // Split test result base on major node version because regex d flag is support from node 16+ - const EXPECTED_TRANSACTION = - major >= 16 - ? { - transaction: 'GET /api/api/v1/sub-router/users/:userId/posts/:postId', - transaction_info: { - source: 'route', - }, - } - : { - transaction: 'GET /api/api/v1/sub-router/users/123/posts/:postId', - transaction_info: { - source: 'route', - }, - }; + const EXPECTED_TRANSACTION = + major >= 16 + ? { + transaction: 'GET /api/api/v1/sub-router/users/:userId/posts/:postId', + transaction_info: { + source: 'route', + }, + } + : { + transaction: 'GET /api/api/v1/sub-router/users/123/posts/:postId', + transaction_info: { + source: 'route', + }, + }; - createRunner(__dirname, 'server.ts') - .ignore('event', 'session', 'sessions') - .expect({ transaction: EXPECTED_TRANSACTION as any }) - .start(done) - .makeRequest('get', '/api/api/v1/sub-router/users/123/posts/456'); -}); + createRunner(__dirname, 'server.ts') + .ignore('event', 'session', 'sessions') + .expect({ transaction: EXPECTED_TRANSACTION as any }) + .start(done) + .makeRequest('get', '/api/api/v1/sub-router/users/123/posts/456'); + }); -test('should construct correct url with multiple parameterized routers, when param is also contain in middle layer route and express used multiple middlewares with route and original url has query params', done => { - // parse node.js major version - const [major] = process.versions.node.split('.').map(Number); - // Split test result base on major node version because regex d flag is support from node 16+ - const EXPECTED_TRANSACTION = - major >= 16 - ? { - transaction: 'GET /api/api/v1/sub-router/users/:userId/posts/:postId', - transaction_info: { - source: 'route', - }, - } - : { - transaction: 'GET /api/api/v1/sub-router/users/123/posts/:postId', - transaction_info: { - source: 'route', - }, - }; + test('should construct correct url with multiple parameterized routers, when param is also contain in middle layer route and express used multiple middlewares with route and original url has query params', done => { + // parse node.js major version + const [major] = process.versions.node.split('.').map(Number); + // Split test result base on major node version because regex d flag is support from node 16+ + const EXPECTED_TRANSACTION = + major >= 16 + ? { + transaction: 'GET /api/api/v1/sub-router/users/:userId/posts/:postId', + transaction_info: { + source: 'route', + }, + } + : { + transaction: 'GET /api/api/v1/sub-router/users/123/posts/:postId', + transaction_info: { + source: 'route', + }, + }; - createRunner(__dirname, 'server.ts') - .ignore('event', 'session', 'sessions') - .expect({ transaction: EXPECTED_TRANSACTION as any }) - .start(done) - .makeRequest('get', '/api/api/v1/sub-router/users/123/posts/456?param=1'); -}); + createRunner(__dirname, 'server.ts') + .ignore('event', 'session', 'sessions') + .expect({ transaction: EXPECTED_TRANSACTION as any }) + .start(done) + .makeRequest('get', '/api/api/v1/sub-router/users/123/posts/456?param=1'); + }); -test('should construct correct url with multiple parameterized routers, when param is also contain in middle layer route and express used multiple middlewares with route and original url ends with trailing slash and has query params', done => { - // parse node.js major version - const [major] = process.versions.node.split('.').map(Number); - // Split test result base on major node version because regex d flag is support from node 16+ - const EXPECTED_TRANSACTION = - major >= 16 - ? { - transaction: 'GET /api/api/v1/sub-router/users/:userId/posts/:postId', - transaction_info: { - source: 'route', - }, - } - : { - transaction: 'GET /api/api/v1/sub-router/users/123/posts/:postId', - transaction_info: { - source: 'route', - }, - }; + test('should construct correct url with multiple parameterized routers, when param is also contain in middle layer route and express used multiple middlewares with route and original url ends with trailing slash and has query params', done => { + // parse node.js major version + const [major] = process.versions.node.split('.').map(Number); + // Split test result base on major node version because regex d flag is support from node 16+ + const EXPECTED_TRANSACTION = + major >= 16 + ? { + transaction: 'GET /api/api/v1/sub-router/users/:userId/posts/:postId', + transaction_info: { + source: 'route', + }, + } + : { + transaction: 'GET /api/api/v1/sub-router/users/123/posts/:postId', + transaction_info: { + source: 'route', + }, + }; - createRunner(__dirname, 'server.ts') - .ignore('event', 'session', 'sessions') - .expect({ transaction: EXPECTED_TRANSACTION as any }) - .start(done) - .makeRequest('get', '/api/api/v1/sub-router/users/123/posts/456/?param=1'); + createRunner(__dirname, 'server.ts') + .ignore('event', 'session', 'sessions') + .expect({ transaction: EXPECTED_TRANSACTION as any }) + .start(done) + .makeRequest('get', '/api/api/v1/sub-router/users/123/posts/456/?param=1'); + }); }); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/middle-layer-parameterized/server.ts b/dev-packages/node-integration-tests/suites/express/multiple-routers/middle-layer-parameterized/server.ts index fbdb8a185c77..0d005f1c55d7 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/middle-layer-parameterized/server.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/middle-layer-parameterized/server.ts @@ -1,19 +1,20 @@ -import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; -import * as Sentry from '@sentry/node-experimental'; -import express from 'express'; - -const app = express(); +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', - integrations: [Sentry.httpIntegration({ tracing: true }), new Sentry.Integrations.Express({ app })], + integrations: [ + // TODO: This used to use the Express integration + ], tracesSampleRate: 1.0, transport: loggingTransport, }); -app.use(Sentry.Handlers.requestHandler()); -app.use(Sentry.Handlers.tracingHandler()); +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import express from 'express'; + +const app = express(); const APIv1 = express.Router(); @@ -30,6 +31,6 @@ const root = express.Router(); app.use('/api/v1', APIv1); app.use('/api', root); -app.use(Sentry.Handlers.errorHandler()); +Sentry.setupExpressErrorHandler(app); startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/middle-layer-parameterized/test.ts b/dev-packages/node-integration-tests/suites/express/multiple-routers/middle-layer-parameterized/test.ts index d42fc925544b..a92d8e738e29 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/middle-layer-parameterized/test.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/middle-layer-parameterized/test.ts @@ -1,31 +1,35 @@ +import { conditionalTest } from '../../../../utils'; import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; afterAll(() => { cleanupChildProcesses(); }); -test('should construct correct url with multiple parameterized routers, when param is also contain in middle layer route', done => { - // parse node.js major version - const [major] = process.versions.node.split('.').map(Number); - // Split test result base on major node version because regex d flag is support from node 16+ - const EXPECTED_TRANSACTION = - major >= 16 - ? { - transaction: 'GET /api/v1/users/:userId/posts/:postId', - transaction_info: { - source: 'route', - }, - } - : { - transaction: 'GET /api/v1/users/123/posts/:postId', - transaction_info: { - source: 'route', - }, - }; +// Before Node 16, parametrization is not working properly here +conditionalTest({ min: 16 })('middle-layer-parameterized', () => { + test('should construct correct url with multiple parameterized routers, when param is also contain in middle layer route', done => { + // parse node.js major version + const [major] = process.versions.node.split('.').map(Number); + // Split test result base on major node version because regex d flag is support from node 16+ + const EXPECTED_TRANSACTION = + major >= 16 + ? { + transaction: 'GET /api/v1/users/:userId/posts/:postId', + transaction_info: { + source: 'route', + }, + } + : { + transaction: 'GET /api/v1/users/123/posts/:postId', + transaction_info: { + source: 'route', + }, + }; - createRunner(__dirname, 'server.ts') - .ignore('event', 'session', 'sessions') - .expect({ transaction: EXPECTED_TRANSACTION as any }) - .start(done) - .makeRequest('get', '/api/v1/users/123/posts/456'); + createRunner(__dirname, 'server.ts') + .ignore('event', 'session', 'sessions') + .expect({ transaction: EXPECTED_TRANSACTION as any }) + .start(done) + .makeRequest('get', '/api/v1/users/123/posts/456'); + }); }); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/server.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/server.ts index e849eca7b9f2..a4a9bf108a95 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/server.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/server.ts @@ -1,10 +1,5 @@ -import http from 'http'; import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; -import * as Sentry from '@sentry/node-experimental'; -import cors from 'cors'; -import express from 'express'; - -const app = express(); +import * as Sentry from '@sentry/node'; export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': string; baggage: string } }; @@ -13,22 +8,26 @@ Sentry.init({ release: '1.0', environment: 'prod', tracePropagationTargets: [/^(?!.*express).*$/], - integrations: [Sentry.httpIntegration({ tracing: true }), new Sentry.Integrations.Express({ app })], + integrations: [ + // TODO: This used to use the Express integration + ], tracesSampleRate: 1.0, transport: loggingTransport, }); -Sentry.setUser({ id: 'user123' }); +import http from 'http'; +import cors from 'cors'; +import express from 'express'; -app.use(Sentry.Handlers.requestHandler()); -app.use(Sentry.Handlers.tracingHandler()); +const app = express(); + +Sentry.setUser({ id: 'user123' }); app.use(cors()); app.get('/test/express', (_req, res) => { - // eslint-disable-next-line deprecation/deprecation - const transaction = Sentry.getCurrentScope().getTransaction(); - const traceId = transaction?.spanContext().traceId; + const span = Sentry.getActiveSpan(); + const traceId = span?.spanContext().traceId; const headers = http.get('http://somewhere.not.sentry/').getHeaders(); if (traceId) { headers['baggage'] = (headers['baggage'] as string).replace(traceId, '__SENTRY_TRACE_ID__'); @@ -37,6 +36,6 @@ app.get('/test/express', (_req, res) => { res.send({ test_data: headers }); }); -app.use(Sentry.Handlers.errorHandler()); +Sentry.setupExpressErrorHandler(app); startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/test.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/test.ts index c3add50d89cd..5a052a454b56 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/test.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/test.ts @@ -11,13 +11,22 @@ test('should attach a baggage header to an outgoing request.', async () => { const response = await runner.makeRequest('get', '/test/express'); expect(response).toBeDefined(); + + const baggage = response?.test_data.baggage?.split(',').sort(); + + expect(baggage).toEqual([ + 'sentry-environment=prod', + 'sentry-public_key=public', + 'sentry-release=1.0', + 'sentry-sample_rate=1', + 'sentry-sampled=true', + 'sentry-trace_id=__SENTRY_TRACE_ID__', + 'sentry-transaction=GET%20%2Ftest%2Fexpress', + ]); + expect(response).toMatchObject({ test_data: { host: 'somewhere.not.sentry', - baggage: - 'sentry-environment=prod,sentry-release=1.0,sentry-public_key=public' + - ',sentry-trace_id=__SENTRY_TRACE_ID__,sentry-sample_rate=1,sentry-transaction=GET%20%2Ftest%2Fexpress' + - ',sentry-sampled=true', }, }); }); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/server.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/server.ts index 4a791a8e73cd..92abb2444294 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/server.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/server.ts @@ -1,10 +1,5 @@ -import * as http from 'http'; -import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; -import * as Sentry from '@sentry/node-experimental'; -import cors from 'cors'; -import express from 'express'; - -const app = express(); +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': string; baggage: string } }; @@ -14,13 +9,19 @@ Sentry.init({ environment: 'prod', // disable requests to /express tracePropagationTargets: [/^(?!.*express).*$/], - integrations: [Sentry.httpIntegration({ tracing: true }), new Sentry.Integrations.Express({ app })], + integrations: [ + // TODO: This used to use the Express integration + ], tracesSampleRate: 1.0, transport: loggingTransport, }); -app.use(Sentry.Handlers.requestHandler()); -app.use(Sentry.Handlers.tracingHandler()); +import * as http from 'http'; +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import cors from 'cors'; +import express from 'express'; + +const app = express(); app.use(cors()); @@ -40,6 +41,6 @@ app.get('/test/express', (_req, res) => { res.send({ test_data: headers }); }); -app.use(Sentry.Handlers.errorHandler()); +Sentry.setupExpressErrorHandler(app); startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts index b7b6c08c0f3e..9af5d4456c89 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts @@ -14,15 +14,24 @@ test('should ignore sentry-values in `baggage` header of a third party vendor an }); expect(response).toBeDefined(); + + const baggage = response?.test_data.baggage?.split(',').sort(); + expect(response).toMatchObject({ test_data: { host: 'somewhere.not.sentry', - baggage: [ - 'other=vendor,foo=bar,third=party,sentry-release=9.9.9,sentry-environment=staging,sentry-sample_rate=0.54,last=item', - 'sentry-release=2.1.0,sentry-environment=myEnv', - ], }, }); + + expect(baggage).toEqual([ + 'foo=bar', + 'last=item', + 'other=vendor', + 'sentry-environment=myEnv', + 'sentry-release=2.1.0', + 'sentry-sample_rate=0.54', + 'third=party', + ]); }); test('should ignore sentry-values in `baggage` header of a third party vendor and overwrite them with new DSC', async () => { @@ -31,15 +40,26 @@ test('should ignore sentry-values in `baggage` header of a third party vendor an const response = await runner.makeRequest('get', '/test/express'); expect(response).toBeDefined(); + + const baggage = response?.test_data.baggage?.split(',').sort(); + expect(response).toMatchObject({ test_data: { host: 'somewhere.not.sentry', - baggage: [ - 'other=vendor,foo=bar,third=party,sentry-release=9.9.9,sentry-environment=staging,sentry-sample_rate=0.54,last=item', - expect.stringMatching( - /sentry-environment=prod,sentry-release=1\.0,sentry-public_key=public,sentry-trace_id=[0-9a-f]{32},sentry-sample_rate=1,sentry-transaction=GET%20%2Ftest%2Fexpress/, - ), - ], }, }); + + expect(baggage).toEqual([ + 'foo=bar', + 'last=item', + 'other=vendor', + 'sentry-environment=prod', + 'sentry-public_key=public', + 'sentry-release=1.0', + 'sentry-sample_rate=1', + 'sentry-sampled=true', + expect.stringMatching(/sentry-trace_id=[0-9a-f]{32}/), + 'sentry-transaction=GET%20%2Ftest%2Fexpress', + 'third=party', + ]); }); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/server.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/server.ts index 5146b809854b..effc44c2b248 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/server.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/server.ts @@ -1,10 +1,5 @@ -import http from 'http'; -import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; -import * as Sentry from '@sentry/node-experimental'; -import cors from 'cors'; -import express from 'express'; - -const app = express(); +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': string; baggage: string } }; @@ -14,13 +9,19 @@ Sentry.init({ environment: 'prod', // disable requests to /express tracePropagationTargets: [/^(?!.*express).*$/], - integrations: [Sentry.httpIntegration({ tracing: true }), new Sentry.Integrations.Express({ app })], + integrations: [ + // TODO: This used to use the Express integration + ], tracesSampleRate: 1.0, transport: loggingTransport, }); -app.use(Sentry.Handlers.requestHandler()); -app.use(Sentry.Handlers.tracingHandler()); +import http from 'http'; +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import cors from 'cors'; +import express from 'express'; + +const app = express(); app.use(cors()); @@ -34,6 +35,6 @@ app.get('/test/express', (_req, res) => { res.send({ test_data: headers }); }); -app.use(Sentry.Handlers.errorHandler()); +Sentry.setupExpressErrorHandler(app); startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/test.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/test.ts index 41b2b9d7cf19..dd3c0f8cddd7 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/test.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/test.ts @@ -17,7 +17,7 @@ test('should merge `baggage` header of a third party vendor with the Sentry DSC expect(response).toMatchObject({ test_data: { host: 'somewhere.not.sentry', - baggage: ['other=vendor,foo=bar,third=party', 'sentry-release=2.0.0,sentry-environment=myEnv'], + baggage: 'other=vendor,foo=bar,third=party,sentry-release=2.0.0,sentry-environment=myEnv', }, }); }); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts index 2e5cfedb8046..ed8f7487a9c3 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts @@ -1,11 +1,5 @@ -import http from 'http'; -import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; -import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; -import * as Sentry from '@sentry/node-experimental'; -import cors from 'cors'; -import express from 'express'; - -const app = express(); +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': string; baggage: string } }; @@ -15,30 +9,32 @@ Sentry.init({ environment: 'prod', // disable requests to /express tracePropagationTargets: [/^(?!.*express).*$/], - integrations: [Sentry.httpIntegration({ tracing: true }), new Sentry.Integrations.Express({ app })], + integrations: [ + // TODO: This used to use the Express integration + ], tracesSampleRate: 1.0, // TODO: We're rethinking the mechanism for including Pii data in DSC, hence commenting out sendDefaultPii for now // sendDefaultPii: true, transport: loggingTransport, }); -Sentry.setUser({ id: 'user123' }); +import http from 'http'; +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import cors from 'cors'; +import express from 'express'; + +const app = express(); -app.use(Sentry.Handlers.requestHandler()); -app.use(Sentry.Handlers.tracingHandler()); +Sentry.setUser({ id: 'user123' }); app.use(cors()); app.get('/test/express', (_req, res) => { - // eslint-disable-next-line deprecation/deprecation - const transaction = Sentry.getCurrentScope().getTransaction(); - transaction?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); - 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()); +Sentry.setupExpressErrorHandler(app); startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/server.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/server.ts index efb9fc3c92bf..b9218b905e9e 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/server.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/server.ts @@ -1,10 +1,5 @@ -import http from 'http'; -import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; -import * as Sentry from '@sentry/node-experimental'; -import cors from 'cors'; -import express from 'express'; - -const app = express(); +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': string; baggage: string } }; @@ -13,13 +8,19 @@ Sentry.init({ release: '1.0', environment: 'prod', tracePropagationTargets: [/^(?!.*express).*$/], - integrations: [Sentry.httpIntegration({ tracing: true }), new Sentry.Integrations.Express({ app })], + integrations: [ + // TODO: This used to use the Express integration + ], tracesSampleRate: 1.0, transport: loggingTransport, }); -app.use(Sentry.Handlers.requestHandler()); -app.use(Sentry.Handlers.tracingHandler()); +import http from 'http'; +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import cors from 'cors'; +import express from 'express'; + +const app = express(); app.use(cors()); @@ -30,6 +31,6 @@ app.get('/test/express', (_req, res) => { res.send({ test_data: headers }); }); -app.use(Sentry.Handlers.errorHandler()); +Sentry.setupExpressErrorHandler(app); startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/tracing-experimental/test.ts b/dev-packages/node-integration-tests/suites/express/tracing-experimental/test.ts deleted file mode 100644 index 337a1166ee64..000000000000 --- a/dev-packages/node-integration-tests/suites/express/tracing-experimental/test.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; - -describe('express tracing experimental', () => { - afterAll(() => { - cleanupChildProcesses(); - }); - - describe('CJS', () => { - test('should create and send transactions for Express routes and spans for middlewares.', done => { - 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\/express$/), - 'http.response.status_code': 200, - }, - op: 'http.server', - status: 'ok', - }, - }, - spans: expect.arrayContaining([ - expect.objectContaining({ - data: expect.objectContaining({ - 'express.name': 'corsMiddleware', - 'express.type': 'middleware', - }), - description: 'middleware - corsMiddleware', - origin: 'auto.http.otel.express', - }), - ]), - }, - }) - .start(done) - .makeRequest('get', '/test/express'); - }); - - test('should set a correct transaction name for routes specified in RegEx', done => { - createRunner(__dirname, 'server.js') - .ignore('session', 'sessions') - .expect({ - transaction: { - transaction: 'GET /', - transaction_info: { - source: 'route', - }, - contexts: { - trace: { - trace_id: expect.any(String), - span_id: expect.any(String), - data: { - url: expect.stringMatching(/\/test\/regex$/), - 'http.response.status_code': 200, - }, - op: 'http.server', - status: 'ok', - }, - }, - }, - }) - .start(done) - .makeRequest('get', '/test/regex'); - }); - - test.each([['array1'], ['array5']])( - 'should set a correct transaction name for routes consisting of arrays of routes', - ((segment: string, done: () => void) => { - createRunner(__dirname, 'server.js') - .ignore('session', 'sessions') - .expect({ - transaction: { - transaction: 'GET /', - transaction_info: { - source: 'route', - }, - contexts: { - trace: { - trace_id: expect.any(String), - span_id: expect.any(String), - data: { - url: expect.stringMatching(`/test/${segment}$`), - 'http.response.status_code': 200, - }, - op: 'http.server', - status: 'ok', - }, - }, - }, - }) - .start(done) - .makeRequest('get', `/test/${segment}`); - }) as any, - ); - - test.each([ - ['arr/545'], - ['arr/required'], - ['arr/required'], - ['arr/requiredPath'], - ['arr/required/lastParam'], - ['arr55/required/lastParam'], - ['arr/requiredPath/optionalPath/'], - ['arr/requiredPath/optionalPath/lastParam'], - ])('should handle more complex regexes in route arrays correctly', ((segment: string, done: () => void) => { - createRunner(__dirname, 'server.js') - .ignore('session', 'sessions') - .expect({ - transaction: { - transaction: 'GET /', - transaction_info: { - source: 'route', - }, - contexts: { - trace: { - trace_id: expect.any(String), - span_id: expect.any(String), - data: { - url: expect.stringMatching(`/test/${segment}$`), - 'http.response.status_code': 200, - }, - op: 'http.server', - status: 'ok', - }, - }, - }, - }) - .start(done) - .makeRequest('get', `/test/${segment}`); - }) as any); - }); -}); diff --git a/dev-packages/node-integration-tests/suites/express/tracing-experimental/server.js b/dev-packages/node-integration-tests/suites/express/tracing/server.js similarity index 84% rename from dev-packages/node-integration-tests/suites/express/tracing-experimental/server.js rename to dev-packages/node-integration-tests/suites/express/tracing/server.js index 06c8416eb5eb..81560806097e 100644 --- a/dev-packages/node-integration-tests/suites/express/tracing-experimental/server.js +++ b/dev-packages/node-integration-tests/suites/express/tracing/server.js @@ -1,6 +1,5 @@ -const { loggingTransport, startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests'); +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); const Sentry = require('@sentry/node'); -const cors = require('cors'); Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', @@ -13,6 +12,8 @@ Sentry.init({ // 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(); diff --git a/dev-packages/node-integration-tests/suites/express/tracing/server.ts b/dev-packages/node-integration-tests/suites/express/tracing/server.ts deleted file mode 100644 index e6faa39956c9..000000000000 --- a/dev-packages/node-integration-tests/suites/express/tracing/server.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; -import * as Sentry from '@sentry/node-experimental'; -import cors from 'cors'; -import express from 'express'; - -const app = express(); - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - // disable attaching headers to /test/* endpoints - tracePropagationTargets: [/^(?!.*test).*$/], - integrations: [Sentry.httpIntegration({ tracing: true }), new Sentry.Integrations.Express({ app })], - tracesSampleRate: 1.0, - transport: loggingTransport, -}); - -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.get(/\/test\/regex/, (_req, res) => { - res.send({ response: 'response 2' }); -}); - -app.get(['/test/array1', /\/test\/array[2-9]/], (_req, res) => { - res.send({ response: 'response 3' }); -}); - -app.get(['/test/arr/:id', /\/test\/arr[0-9]*\/required(path)?(\/optionalPath)?\/(lastParam)?/], (_req, res) => { - res.send({ response: 'response 4' }); -}); - -app.use(Sentry.Handlers.errorHandler()); - -startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/tracing/test.ts b/dev-packages/node-integration-tests/suites/express/tracing/test.ts index 5c9e7743636f..337a1166ee64 100644 --- a/dev-packages/node-integration-tests/suites/express/tracing/test.ts +++ b/dev-packages/node-integration-tests/suites/express/tracing/test.ts @@ -1,127 +1,135 @@ import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; -afterAll(() => { - cleanupChildProcesses(); -}); +describe('express tracing experimental', () => { + afterAll(() => { + cleanupChildProcesses(); + }); -test('should create and send transactions for Express routes and spans for middlewares.', done => { - createRunner(__dirname, 'server.ts') - .ignore('session', 'sessions') - .expect({ - transaction: { - contexts: { - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - data: { - url: '/test/express', - 'http.response.status_code': 200, + describe('CJS', () => { + test('should create and send transactions for Express routes and spans for middlewares.', done => { + 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\/express$/), + 'http.response.status_code': 200, + }, + op: 'http.server', + status: 'ok', + }, }, - op: 'http.server', - status: 'ok', + spans: expect.arrayContaining([ + expect.objectContaining({ + data: expect.objectContaining({ + 'express.name': 'corsMiddleware', + 'express.type': 'middleware', + }), + description: 'middleware - corsMiddleware', + origin: 'auto.http.otel.express', + }), + ]), }, - }, - spans: [ - expect.objectContaining({ - description: 'corsMiddleware', - op: 'middleware.express.use', - }), - ], - }, - }) - .start(done) - .makeRequest('get', '/test/express'); -}); + }) + .start(done) + .makeRequest('get', '/test/express'); + }); -test('should set a correct transaction name for routes specified in RegEx', done => { - createRunner(__dirname, 'server.ts') - .ignore('session', 'sessions') - .expect({ - transaction: { - transaction: 'GET /\\/test\\/regex/', - transaction_info: { - source: 'route', - }, - contexts: { - trace: { - trace_id: expect.any(String), - span_id: expect.any(String), - data: { - url: '/test/regex', - 'http.response.status_code': 200, + test('should set a correct transaction name for routes specified in RegEx', done => { + createRunner(__dirname, 'server.js') + .ignore('session', 'sessions') + .expect({ + transaction: { + transaction: 'GET /', + transaction_info: { + source: 'route', + }, + contexts: { + trace: { + trace_id: expect.any(String), + span_id: expect.any(String), + data: { + url: expect.stringMatching(/\/test\/regex$/), + 'http.response.status_code': 200, + }, + op: 'http.server', + status: 'ok', + }, }, - op: 'http.server', - status: 'ok', }, - }, - }, - }) - .start(done) - .makeRequest('get', '/test/regex'); -}); + }) + .start(done) + .makeRequest('get', '/test/regex'); + }); -test.each([['array1'], ['array5']])( - 'should set a correct transaction name for routes consisting of arrays of routes', - ((segment: string, done: () => void) => { - createRunner(__dirname, 'server.ts') - .ignore('session', 'sessions') - .expect({ - transaction: { - transaction: 'GET /test/array1,/\\/test\\/array[2-9]', - transaction_info: { - source: 'route', - }, - contexts: { - trace: { - trace_id: expect.any(String), - span_id: expect.any(String), - data: { - url: `/test/${segment}`, - 'http.response.status_code': 200, + test.each([['array1'], ['array5']])( + 'should set a correct transaction name for routes consisting of arrays of routes', + ((segment: string, done: () => void) => { + createRunner(__dirname, 'server.js') + .ignore('session', 'sessions') + .expect({ + transaction: { + transaction: 'GET /', + transaction_info: { + source: 'route', + }, + contexts: { + trace: { + trace_id: expect.any(String), + span_id: expect.any(String), + data: { + url: expect.stringMatching(`/test/${segment}$`), + 'http.response.status_code': 200, + }, + op: 'http.server', + status: 'ok', + }, }, - op: 'http.server', - status: 'ok', }, - }, - }, - }) - .start(done) - .makeRequest('get', `/test/${segment}`); - }) as any, -); + }) + .start(done) + .makeRequest('get', `/test/${segment}`); + }) as any, + ); -test.each([ - ['arr/545'], - ['arr/required'], - ['arr/required'], - ['arr/requiredPath'], - ['arr/required/lastParam'], - ['arr55/required/lastParam'], - ['arr/requiredPath/optionalPath/'], - ['arr/requiredPath/optionalPath/lastParam'], -])('should handle more complex regexes in route arrays correctly', ((segment: string, done: () => void) => { - createRunner(__dirname, 'server.ts') - .ignore('session', 'sessions') - .expect({ - transaction: { - transaction: 'GET /test/arr/:id,/\\/test\\/arr[0-9]*\\/required(path)?(\\/optionalPath)?\\/(lastParam)?', - transaction_info: { - source: 'route', - }, - contexts: { - trace: { - trace_id: expect.any(String), - span_id: expect.any(String), - data: { - url: `/test/${segment}`, - 'http.response.status_code': 200, + test.each([ + ['arr/545'], + ['arr/required'], + ['arr/required'], + ['arr/requiredPath'], + ['arr/required/lastParam'], + ['arr55/required/lastParam'], + ['arr/requiredPath/optionalPath/'], + ['arr/requiredPath/optionalPath/lastParam'], + ])('should handle more complex regexes in route arrays correctly', ((segment: string, done: () => void) => { + createRunner(__dirname, 'server.js') + .ignore('session', 'sessions') + .expect({ + transaction: { + transaction: 'GET /', + transaction_info: { + source: 'route', + }, + contexts: { + trace: { + trace_id: expect.any(String), + span_id: expect.any(String), + data: { + url: expect.stringMatching(`/test/${segment}$`), + 'http.response.status_code': 200, + }, + op: 'http.server', + status: 'ok', + }, }, - op: 'http.server', - status: 'ok', }, - }, - }, - }) - .start(done) - .makeRequest('get', `/test/${segment}`); -}) as any); + }) + .start(done) + .makeRequest('get', `/test/${segment}`); + }) as any); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/metrics/should-exit-forced.js b/dev-packages/node-integration-tests/suites/metrics/should-exit-forced.js new file mode 100644 index 000000000000..2621828973ab --- /dev/null +++ b/dev-packages/node-integration-tests/suites/metrics/should-exit-forced.js @@ -0,0 +1,19 @@ +const Sentry = require('@sentry/node'); + +function configureSentry() { + Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + autoSessionTracking: false, + }); + + Sentry.metrics.increment('test'); +} + +async function main() { + configureSentry(); + await new Promise(resolve => setTimeout(resolve, 1000)); + process.exit(0); +} + +main(); diff --git a/dev-packages/node-integration-tests/suites/metrics/should-exit.js b/dev-packages/node-integration-tests/suites/metrics/should-exit.js new file mode 100644 index 000000000000..01a6f0194507 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/metrics/should-exit.js @@ -0,0 +1,18 @@ +const Sentry = require('@sentry/node'); + +function configureSentry() { + Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + autoSessionTracking: false, + }); + + Sentry.metrics.increment('test'); +} + +async function main() { + configureSentry(); + await new Promise(resolve => setTimeout(resolve, 1000)); +} + +main(); diff --git a/dev-packages/node-integration-tests/suites/metrics/test.ts b/dev-packages/node-integration-tests/suites/metrics/test.ts new file mode 100644 index 000000000000..2c3cc350eeba --- /dev/null +++ b/dev-packages/node-integration-tests/suites/metrics/test.ts @@ -0,0 +1,21 @@ +import { createRunner } from '../../utils/runner'; + +describe('metrics', () => { + test('should exit', done => { + const runner = createRunner(__dirname, 'should-exit.js').start(); + + setTimeout(() => { + expect(runner.childHasExited()).toBe(true); + done(); + }, 5_000); + }); + + test('should exit forced', done => { + const runner = createRunner(__dirname, 'should-exit-forced.js').start(); + + setTimeout(() => { + expect(runner.childHasExited()).toBe(true); + done(); + }, 5_000); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/empty-obj/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/empty-obj/scenario.ts index 2bf61debf816..1e2420a3c476 100644 --- a/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/empty-obj/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/empty-obj/scenario.ts @@ -1,8 +1,10 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + transport: loggingTransport, }); Sentry.addBreadcrumb({}); diff --git a/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/empty-obj/test.ts b/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/empty-obj/test.ts index 99392c290924..224c0dc5cd7d 100644 --- a/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/empty-obj/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/empty-obj/test.ts @@ -1,12 +1,15 @@ -import { TestEnv, assertSentryEvent } from '../../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; -test('should add an empty breadcrumb, when an empty object is given', async () => { - const env = await TestEnv.init(__dirname); - const envelope = await env.getEnvelopeRequest(); - - expect(envelope).toHaveLength(3); +afterAll(() => { + cleanupChildProcesses(); +}); - assertSentryEvent(envelope[2], { - message: 'test-empty-obj', - }); +test('should add an empty breadcrumb, when an empty object is given', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'test-empty-obj', + }, + }) + .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/scenario.ts index b9ae05edefac..1b790c839bbd 100644 --- a/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/scenario.ts @@ -1,8 +1,10 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + transport: loggingTransport, }); Sentry.addBreadcrumb({ diff --git a/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/test.ts b/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/test.ts index 79feca7d7b9e..9ee47204c635 100644 --- a/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/test.ts @@ -1,20 +1,25 @@ -import { TestEnv, assertSentryEvent } from '../../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; -test('should add multiple breadcrumbs', async () => { - const env = await TestEnv.init(__dirname); - const events = await env.getEnvelopeRequest(); +afterAll(() => { + cleanupChildProcesses(); +}); - assertSentryEvent(events[2], { - message: 'test_multi_breadcrumbs', - breadcrumbs: [ - { - category: 'foo', - message: 'bar', - level: 'fatal', - }, - { - category: 'qux', +test('should add multiple breadcrumbs', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'test_multi_breadcrumbs', + breadcrumbs: [ + { + category: 'foo', + message: 'bar', + level: 'fatal', + }, + { + category: 'qux', + }, + ], }, - ], - }); + }) + .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/scenario.ts index 94ebd23dfaa5..f4245dfaa0e6 100644 --- a/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/scenario.ts @@ -1,8 +1,10 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + transport: loggingTransport, }); Sentry.addBreadcrumb({ diff --git a/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/test.ts b/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/test.ts index b405549ffdaf..58b2c56f42fd 100644 --- a/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/test.ts @@ -1,17 +1,18 @@ -import { TestEnv, assertSentryEvent } from '../../../../utils'; +import { createRunner } from '../../../../utils/runner'; -test('should add a simple breadcrumb', async () => { - const env = await TestEnv.init(__dirname); - const event = await env.getEnvelopeRequest(); - - assertSentryEvent(event[2], { - message: 'test_simple', - breadcrumbs: [ - { - category: 'foo', - message: 'bar', - level: 'fatal', +test('should add a simple breadcrumb', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'test_simple', + breadcrumbs: [ + { + category: 'foo', + message: 'bar', + level: 'fatal', + }, + ], }, - ], - }); + }) + .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/captureException/catched-error/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/captureException/catched-error/scenario.ts index 0318461ab9c7..0c504121410a 100644 --- a/dev-packages/node-integration-tests/suites/public-api/captureException/catched-error/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/captureException/catched-error/scenario.ts @@ -1,8 +1,10 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + transport: loggingTransport, }); try { diff --git a/dev-packages/node-integration-tests/suites/public-api/captureException/catched-error/test.ts b/dev-packages/node-integration-tests/suites/public-api/captureException/catched-error/test.ts index d47b03efd21b..6d04fee01291 100644 --- a/dev-packages/node-integration-tests/suites/public-api/captureException/catched-error/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/captureException/catched-error/test.ts @@ -1,38 +1,43 @@ -import { TestEnv, assertSentryEvent } from '../../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; -test('should work inside catch block', async () => { - const env = await TestEnv.init(__dirname); - const event = await env.getEnvelopeRequest(); +afterAll(() => { + cleanupChildProcesses(); +}); - assertSentryEvent(event[2], { - exception: { - values: [ - { - type: 'Error', - value: 'catched_error', - mechanism: { - type: 'generic', - handled: true, - }, - stacktrace: { - frames: expect.arrayContaining([ - expect.objectContaining({ - context_line: " throw new Error('catched_error');", - pre_context: [ - '', - 'Sentry.init({', - " dsn: 'https://public@dsn.ingest.sentry.io/1337',", - " release: '1.0',", - '});', - '', - 'try {', - ], - post_context: ['} catch (err) {', ' Sentry.captureException(err);', '}', ''], - }), - ]), - }, +test('should work inside catch block', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + exception: { + values: [ + { + type: 'Error', + value: 'catched_error', + mechanism: { + type: 'generic', + handled: true, + }, + stacktrace: { + frames: expect.arrayContaining([ + expect.objectContaining({ + context_line: " throw new Error('catched_error');", + pre_context: [ + 'Sentry.init({', + " dsn: 'https://public@dsn.ingest.sentry.io/1337',", + " release: '1.0',", + ' transport: loggingTransport,', + '});', + '', + 'try {', + ], + post_context: ['} catch (err) {', ' Sentry.captureException(err);', '}', ''], + }), + ]), + }, + }, + ], }, - ], - }, - }); + }, + }) + .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/captureException/empty-obj/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/captureException/empty-obj/scenario.ts index 87a4b58cdd33..68c51dbfc3a2 100644 --- a/dev-packages/node-integration-tests/suites/public-api/captureException/empty-obj/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/captureException/empty-obj/scenario.ts @@ -1,8 +1,10 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + transport: loggingTransport, }); Sentry.captureException({}); diff --git a/dev-packages/node-integration-tests/suites/public-api/captureException/empty-obj/test.ts b/dev-packages/node-integration-tests/suites/public-api/captureException/empty-obj/test.ts index 8d27b5db2548..4efab7398cb6 100644 --- a/dev-packages/node-integration-tests/suites/public-api/captureException/empty-obj/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/captureException/empty-obj/test.ts @@ -1,21 +1,26 @@ -import { TestEnv, assertSentryEvent } from '../../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; -test('should capture an empty object', async () => { - const env = await TestEnv.init(__dirname); - const event = await env.getEnvelopeRequest(); +afterAll(() => { + cleanupChildProcesses(); +}); - assertSentryEvent(event[2], { - exception: { - values: [ - { - type: 'Error', - value: 'Object captured as exception with keys: [object has no keys]', - mechanism: { - type: 'generic', - handled: true, - }, +test('should capture an empty object', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + exception: { + values: [ + { + type: 'Error', + value: 'Object captured as exception with keys: [object has no keys]', + mechanism: { + type: 'generic', + handled: true, + }, + }, + ], }, - ], - }, - }); + }, + }) + .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/captureException/simple-error/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/captureException/simple-error/scenario.ts index 44366bd34b89..519213a755c5 100644 --- a/dev-packages/node-integration-tests/suites/public-api/captureException/simple-error/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/captureException/simple-error/scenario.ts @@ -1,8 +1,10 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + transport: loggingTransport, }); Sentry.captureException(new Error('test_simple_error')); diff --git a/dev-packages/node-integration-tests/suites/public-api/captureException/simple-error/test.ts b/dev-packages/node-integration-tests/suites/public-api/captureException/simple-error/test.ts index e12a2d4dec16..647edb7c4a13 100644 --- a/dev-packages/node-integration-tests/suites/public-api/captureException/simple-error/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/captureException/simple-error/test.ts @@ -1,24 +1,29 @@ -import { TestEnv, assertSentryEvent } from '../../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; -test('should capture a simple error with message', async () => { - const env = await TestEnv.init(__dirname); - const envelope = await env.getEnvelopeRequest(); +afterAll(() => { + cleanupChildProcesses(); +}); - assertSentryEvent(envelope[2], { - exception: { - values: [ - { - type: 'Error', - value: 'test_simple_error', - mechanism: { - type: 'generic', - handled: true, - }, - stacktrace: { - frames: expect.any(Array), - }, +test('should capture a simple error with message', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + exception: { + values: [ + { + type: 'Error', + value: 'test_simple_error', + mechanism: { + type: 'generic', + handled: true, + }, + stacktrace: { + frames: expect.any(Array), + }, + }, + ], }, - ], - }, - }); + }, + }) + .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/captureMessage/parameterized_message/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/captureMessage/parameterized_message/scenario.ts index 1d92f9dcd769..d6951e4859ec 100644 --- a/dev-packages/node-integration-tests/suites/public-api/captureMessage/parameterized_message/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/captureMessage/parameterized_message/scenario.ts @@ -1,8 +1,10 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + transport: loggingTransport, }); const x = 'first'; diff --git a/dev-packages/node-integration-tests/suites/public-api/captureMessage/parameterized_message/test.ts b/dev-packages/node-integration-tests/suites/public-api/captureMessage/parameterized_message/test.ts index d9015987187f..c67dbc7bb9ce 100644 --- a/dev-packages/node-integration-tests/suites/public-api/captureMessage/parameterized_message/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/captureMessage/parameterized_message/test.ts @@ -1,13 +1,18 @@ -import { TestEnv, assertSentryEvent } from '../../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; -test('should capture a parameterized representation of the message', async () => { - const env = await TestEnv.init(__dirname); - const event = await env.getEnvelopeRequest(); +afterAll(() => { + cleanupChildProcesses(); +}); - assertSentryEvent(event[2], { - logentry: { - message: 'This is a log statement with %s and %s params', - params: ['first', 'second'], - }, - }); +test('should capture a parameterized representation of the message', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + logentry: { + message: 'This is a log statement with %s and %s params', + params: ['first', 'second'], + }, + }, + }) + .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message/scenario.ts index e358175e8702..63c21fe2560d 100644 --- a/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message/scenario.ts @@ -1,8 +1,10 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + transport: loggingTransport, }); Sentry.captureMessage('Message'); diff --git a/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message/test.ts b/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message/test.ts index 876fd978b6b3..6e53e6eb7279 100644 --- a/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message/test.ts @@ -1,11 +1,16 @@ -import { TestEnv, assertSentryEvent } from '../../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; -test('should capture a simple message string', async () => { - const env = await TestEnv.init(__dirname); - const event = await env.getEnvelopeRequest(); +afterAll(() => { + cleanupChildProcesses(); +}); - assertSentryEvent(event[2], { - message: 'Message', - level: 'info', - }); +test('should capture a simple message string', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'Message', + level: 'info', + }, + }) + .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/captureMessage/with_level/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/captureMessage/with_level/scenario.ts index be175d36e816..782dff96e83c 100644 --- a/dev-packages/node-integration-tests/suites/public-api/captureMessage/with_level/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/captureMessage/with_level/scenario.ts @@ -1,8 +1,10 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + transport: loggingTransport, }); Sentry.captureMessage('debug_message', 'debug'); diff --git a/dev-packages/node-integration-tests/suites/public-api/captureMessage/with_level/test.ts b/dev-packages/node-integration-tests/suites/public-api/captureMessage/with_level/test.ts index c4ab3d189cf7..a0f16b2f7b78 100644 --- a/dev-packages/node-integration-tests/suites/public-api/captureMessage/with_level/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/captureMessage/with_level/test.ts @@ -1,36 +1,16 @@ -import { TestEnv, assertSentryEvent } from '../../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; -test('should capture with different severity levels', async () => { - const env = await TestEnv.init(__dirname); - const events = await env.getMultipleEnvelopeRequest({ count: 6 }); - - assertSentryEvent(events[0][2], { - message: 'debug_message', - level: 'debug', - }); - - assertSentryEvent(events[1][2], { - message: 'info_message', - level: 'info', - }); - - assertSentryEvent(events[2][2], { - message: 'warning_message', - level: 'warning', - }); - - assertSentryEvent(events[3][2], { - message: 'error_message', - level: 'error', - }); - - assertSentryEvent(events[4][2], { - message: 'fatal_message', - level: 'fatal', - }); +afterAll(() => { + cleanupChildProcesses(); +}); - assertSentryEvent(events[5][2], { - message: 'log_message', - level: 'log', - }); +test('should capture with different severity levels', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ event: { message: 'debug_message', level: 'debug' } }) + .expect({ event: { message: 'info_message', level: 'info' } }) + .expect({ event: { message: 'warning_message', level: 'warning' } }) + .expect({ event: { message: 'error_message', level: 'error' } }) + .expect({ event: { message: 'fatal_message', level: 'fatal' } }) + .expect({ event: { message: 'log_message', level: 'log' } }) + .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/configureScope/clear_scope/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/configureScope/clear_scope/scenario.ts index 588c56c273e9..d40b9c044f86 100644 --- a/dev-packages/node-integration-tests/suites/public-api/configureScope/clear_scope/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/configureScope/clear_scope/scenario.ts @@ -1,8 +1,10 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + transport: loggingTransport, }); const scope = Sentry.getCurrentScope(); diff --git a/dev-packages/node-integration-tests/suites/public-api/configureScope/clear_scope/test.ts b/dev-packages/node-integration-tests/suites/public-api/configureScope/clear_scope/test.ts index e575ab06faae..234a5fed56ad 100644 --- a/dev-packages/node-integration-tests/suites/public-api/configureScope/clear_scope/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/configureScope/clear_scope/test.ts @@ -1,14 +1,15 @@ -import type { Event } from '@sentry/node'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; -import { TestEnv, assertSentryEvent } from '../../../../utils'; - -test('should clear previously set properties of a scope', async () => { - const env = await TestEnv.init(__dirname); - const envelope = await env.getEnvelopeRequest(); - - assertSentryEvent(envelope[2], { - message: 'cleared_scope', - }); +afterAll(() => { + cleanupChildProcesses(); +}); - expect((envelope[2] as Event).user).not.toBeDefined(); +test('should clear previously set properties of a scope', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'cleared_scope', + }, + }) + .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/configureScope/set_properties/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/configureScope/set_properties/scenario.ts index b3f3f4d4ae15..69d301ea1196 100644 --- a/dev-packages/node-integration-tests/suites/public-api/configureScope/set_properties/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/configureScope/set_properties/scenario.ts @@ -1,8 +1,10 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + transport: loggingTransport, }); const scope = Sentry.getCurrentScope(); diff --git a/dev-packages/node-integration-tests/suites/public-api/configureScope/set_properties/test.ts b/dev-packages/node-integration-tests/suites/public-api/configureScope/set_properties/test.ts index 0f89eb334800..bb07ce4190d9 100644 --- a/dev-packages/node-integration-tests/suites/public-api/configureScope/set_properties/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/configureScope/set_properties/test.ts @@ -1,19 +1,24 @@ -import { TestEnv, assertSentryEvent } from '../../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; -test('should set different properties of a scope', async () => { - const env = await TestEnv.init(__dirname); - const envelope = await env.getEnvelopeRequest(); +afterAll(() => { + cleanupChildProcesses(); +}); - assertSentryEvent(envelope[2], { - message: 'configured_scope', - tags: { - foo: 'bar', - }, - extra: { - qux: 'quux', - }, - user: { - id: 'baz', - }, - }); +test('should set different properties of a scope', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'configured_scope', + tags: { + foo: 'bar', + }, + extra: { + qux: 'quux', + }, + user: { + id: 'baz', + }, + }, + }) + .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/scopes/initialScopes/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/scopes/initialScopes/scenario.ts index 759206f761fc..dc068a09b744 100644 --- a/dev-packages/node-integration-tests/suites/public-api/scopes/initialScopes/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/scopes/initialScopes/scenario.ts @@ -1,8 +1,10 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + transport: loggingTransport, }); const globalScope = Sentry.getGlobalScope(); diff --git a/dev-packages/node-integration-tests/suites/public-api/scopes/initialScopes/test.ts b/dev-packages/node-integration-tests/suites/public-api/scopes/initialScopes/test.ts index 069285c452c7..d9b7bc8b2fc9 100644 --- a/dev-packages/node-integration-tests/suites/public-api/scopes/initialScopes/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/scopes/initialScopes/test.ts @@ -1,31 +1,38 @@ -import { TestEnv, assertSentryEvent } from '../../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; -test('should apply scopes correctly', async () => { - const env = await TestEnv.init(__dirname); - const events = await env.getMultipleEnvelopeRequest({ count: 3 }); - - assertSentryEvent(events[0][2], { - message: 'outer_before', - extra: { - aa: 'aa', - bb: 'bb', - }, - }); - - assertSentryEvent(events[1][2], { - message: 'inner', - extra: { - aa: 'aa', - bb: 'bb', - cc: 'cc', - }, - }); +afterAll(() => { + cleanupChildProcesses(); +}); - assertSentryEvent(events[2][2], { - message: 'outer_after', - extra: { - aa: 'aa', - bb: 'bb', - }, - }); +test('should apply scopes correctly', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'outer_before', + extra: { + aa: 'aa', + bb: 'bb', + }, + }, + }) + .expect({ + event: { + message: 'inner', + extra: { + aa: 'aa', + bb: 'bb', + cc: 'cc', + }, + }, + }) + .expect({ + event: { + message: 'outer_after', + extra: { + aa: 'aa', + bb: 'bb', + }, + }, + }) + .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/scopes/isolationScope/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/scopes/isolationScope/scenario.ts index d38b96b91d4e..eec2e911b609 100644 --- a/dev-packages/node-integration-tests/suites/public-api/scopes/isolationScope/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/scopes/isolationScope/scenario.ts @@ -1,8 +1,10 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + transport: loggingTransport, }); const globalScope = Sentry.getGlobalScope(); diff --git a/dev-packages/node-integration-tests/suites/public-api/scopes/isolationScope/test.ts b/dev-packages/node-integration-tests/suites/public-api/scopes/isolationScope/test.ts index 4288d59bb799..85722a870c09 100644 --- a/dev-packages/node-integration-tests/suites/public-api/scopes/isolationScope/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/scopes/isolationScope/test.ts @@ -1,47 +1,55 @@ -import { TestEnv, assertSentryEvent } from '../../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; -test('should apply scopes correctly', async () => { - const env = await TestEnv.init(__dirname); - const events = await env.getMultipleEnvelopeRequest({ count: 4 }); - - assertSentryEvent(events[0][2], { - message: 'outer_before', - extra: { - aa: 'aa', - bb: 'bb', - }, - }); - - assertSentryEvent(events[1][2], { - message: 'inner', - extra: { - aa: 'aa', - bb: 'bb', - cc: 'cc', - dd: 'dd', - ee: 'ee', - }, - }); - - assertSentryEvent(events[2][2], { - message: 'inner_async_context', - extra: { - aa: 'aa', - bb: 'bb', - cc: 'cc', - dd: 'dd', - ff: 'ff', - gg: 'gg', - }, - }); +afterAll(() => { + cleanupChildProcesses(); +}); - assertSentryEvent(events[3][2], { - message: 'outer_after', - extra: { - aa: 'aa', - bb: 'bb', - cc: 'cc', - dd: 'dd', - }, - }); +test('should apply scopes correctly', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'outer_before', + extra: { + aa: 'aa', + bb: 'bb', + }, + }, + }) + .expect({ + event: { + message: 'inner', + extra: { + aa: 'aa', + bb: 'bb', + cc: 'cc', + dd: 'dd', + ee: 'ee', + }, + }, + }) + .expect({ + event: { + message: 'inner_async_context', + extra: { + aa: 'aa', + bb: 'bb', + cc: 'cc', + dd: 'dd', + ff: 'ff', + gg: 'gg', + }, + }, + }) + .expect({ + event: { + message: 'outer_after', + extra: { + aa: 'aa', + bb: 'bb', + cc: 'cc', + dd: 'dd', + }, + }, + }) + .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/setContext/multiple-contexts/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setContext/multiple-contexts/scenario.ts index b0e44a65943c..16a557a17526 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setContext/multiple-contexts/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setContext/multiple-contexts/scenario.ts @@ -1,8 +1,10 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + transport: loggingTransport, }); Sentry.setContext('context_1', { diff --git a/dev-packages/node-integration-tests/suites/public-api/setContext/multiple-contexts/test.ts b/dev-packages/node-integration-tests/suites/public-api/setContext/multiple-contexts/test.ts index db449f8e3cf3..4ede2800470c 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setContext/multiple-contexts/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setContext/multiple-contexts/test.ts @@ -1,21 +1,22 @@ -import type { Event } from '@sentry/node'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; -import { TestEnv, assertSentryEvent } from '../../../../utils'; - -test('should record multiple contexts', async () => { - const env = await TestEnv.init(__dirname); - const envelope = await env.getEnvelopeRequest(); +afterAll(() => { + cleanupChildProcesses(); +}); - assertSentryEvent(envelope[2], { - message: 'multiple_contexts', - contexts: { - context_1: { - foo: 'bar', - baz: { qux: 'quux' }, +test('should record multiple contexts', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'multiple_contexts', + contexts: { + context_1: { + foo: 'bar', + baz: { qux: 'quux' }, + }, + context_2: { 1: 'foo', bar: false }, + }, }, - context_2: { 1: 'foo', bar: false }, - }, - }); - - expect((envelope[0] as Event).contexts?.context_3).not.toBeDefined(); + }) + .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/setContext/non-serializable-context/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setContext/non-serializable-context/scenario.ts index 52fec94ab6e4..28a10e4a554b 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setContext/non-serializable-context/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setContext/non-serializable-context/scenario.ts @@ -1,8 +1,10 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + transport: loggingTransport, }); type Circular = { diff --git a/dev-packages/node-integration-tests/suites/public-api/setContext/non-serializable-context/test.ts b/dev-packages/node-integration-tests/suites/public-api/setContext/non-serializable-context/test.ts index 289715e2bc06..17c265b00922 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setContext/non-serializable-context/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setContext/non-serializable-context/test.ts @@ -1,15 +1,11 @@ -import type { Event } from '@sentry/node'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; -import { TestEnv, assertSentryEvent } from '../../../../utils'; - -test('should normalize non-serializable context', async () => { - const env = await TestEnv.init(__dirname); - const event = await env.getEnvelopeRequest(); - - assertSentryEvent(event[2], { - message: 'non_serializable', - contexts: {}, - }); +afterAll(() => { + cleanupChildProcesses(); +}); - expect((event[0] as Event).contexts?.context_3).not.toBeDefined(); +test('should normalize non-serializable context', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ event: { message: 'non_serializable', contexts: {} } }) + .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/setContext/simple-context/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setContext/simple-context/scenario.ts index 43713f2768fb..658df9c2b20f 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setContext/simple-context/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setContext/simple-context/scenario.ts @@ -1,8 +1,10 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + transport: loggingTransport, }); Sentry.setContext('foo', { bar: 'baz' }); diff --git a/dev-packages/node-integration-tests/suites/public-api/setContext/simple-context/test.ts b/dev-packages/node-integration-tests/suites/public-api/setContext/simple-context/test.ts index a8d3588a2c9c..a67efb9148bc 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setContext/simple-context/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setContext/simple-context/test.ts @@ -1,19 +1,20 @@ -import type { Event } from '@sentry/node'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; -import { TestEnv, assertSentryEvent } from '../../../../utils'; - -test('should set a simple context', async () => { - const env = await TestEnv.init(__dirname); - const envelope = await env.getEnvelopeRequest(); +afterAll(() => { + cleanupChildProcesses(); +}); - assertSentryEvent(envelope[2], { - message: 'simple_context_object', - contexts: { - foo: { - bar: 'baz', +test('should set a simple context', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'simple_context_object', + contexts: { + foo: { + bar: 'baz', + }, + }, }, - }, - }); - - expect((envelope[2] as Event).contexts?.context_3).not.toBeDefined(); + }) + .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/setExtra/multiple-extras/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setExtra/multiple-extras/scenario.ts index 67d061e8f9e3..fa32edbf0329 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setExtra/multiple-extras/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setExtra/multiple-extras/scenario.ts @@ -1,8 +1,10 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + transport: loggingTransport, }); Sentry.setExtra('extra_1', { diff --git a/dev-packages/node-integration-tests/suites/public-api/setExtra/multiple-extras/test.ts b/dev-packages/node-integration-tests/suites/public-api/setExtra/multiple-extras/test.ts index 10b398b36fd3..feb55d69f8a4 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setExtra/multiple-extras/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setExtra/multiple-extras/test.ts @@ -1,14 +1,19 @@ -import { TestEnv, assertSentryEvent } from '../../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; -test('should record multiple extras of different types', async () => { - const env = await TestEnv.init(__dirname); - const event = await env.getEnvelopeRequest(); +afterAll(() => { + cleanupChildProcesses(); +}); - assertSentryEvent(event[2], { - message: 'multiple_extras', - extra: { - extra_1: { foo: 'bar', baz: { qux: 'quux' } }, - extra_2: false, - }, - }); +test('should record multiple extras of different types', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'multiple_extras', + extra: { + extra_1: { foo: 'bar', baz: { qux: 'quux' } }, + extra_2: false, + }, + }, + }) + .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/setExtra/non-serializable-extra/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setExtra/non-serializable-extra/scenario.ts index a09fc9d7dd9c..00daf54f1389 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setExtra/non-serializable-extra/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setExtra/non-serializable-extra/scenario.ts @@ -1,8 +1,10 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + transport: loggingTransport, }); type Circular = { diff --git a/dev-packages/node-integration-tests/suites/public-api/setExtra/non-serializable-extra/test.ts b/dev-packages/node-integration-tests/suites/public-api/setExtra/non-serializable-extra/test.ts index 439a91db98d1..f785f44f2d38 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setExtra/non-serializable-extra/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setExtra/non-serializable-extra/test.ts @@ -1,11 +1,16 @@ -import { TestEnv, assertSentryEvent } from '../../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; -test('should normalize non-serializable extra', async () => { - const env = await TestEnv.init(__dirname); - const event = await env.getEnvelopeRequest(); +afterAll(() => { + cleanupChildProcesses(); +}); - assertSentryEvent(event[2], { - message: 'non_serializable', - extra: {}, - }); +test('should normalize non-serializable extra', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'non_serializable', + extra: {}, + }, + }) + .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/setExtra/simple-extra/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setExtra/simple-extra/scenario.ts index 6c4fce288972..231e622dd3f8 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setExtra/simple-extra/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setExtra/simple-extra/scenario.ts @@ -1,8 +1,10 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + transport: loggingTransport, }); Sentry.setExtra('foo', { diff --git a/dev-packages/node-integration-tests/suites/public-api/setExtra/simple-extra/test.ts b/dev-packages/node-integration-tests/suites/public-api/setExtra/simple-extra/test.ts index dbac8bdd0607..3180910e5a10 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setExtra/simple-extra/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setExtra/simple-extra/test.ts @@ -1,18 +1,23 @@ -import { TestEnv, assertSentryEvent } from '../../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; -test('should set a simple extra', async () => { - const env = await TestEnv.init(__dirname); - const event = await env.getEnvelopeRequest(); +afterAll(() => { + cleanupChildProcesses(); +}); - assertSentryEvent(event[2], { - message: 'simple_extra', - extra: { - foo: { - foo: 'bar', - baz: { - qux: 'quux', +test('should set a simple extra', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'simple_extra', + extra: { + foo: { + foo: 'bar', + baz: { + qux: 'quux', + }, + }, }, }, - }, - }); + }) + .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/setExtras/consecutive-calls/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setExtras/consecutive-calls/scenario.ts index 3c75dbe3f119..b122e99a0d50 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setExtras/consecutive-calls/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setExtras/consecutive-calls/scenario.ts @@ -1,8 +1,10 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + transport: loggingTransport, }); Sentry.setExtras({ extra: [] }); diff --git a/dev-packages/node-integration-tests/suites/public-api/setExtras/consecutive-calls/test.ts b/dev-packages/node-integration-tests/suites/public-api/setExtras/consecutive-calls/test.ts index fd3d00a0b5da..c6e3cd04c475 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setExtras/consecutive-calls/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setExtras/consecutive-calls/test.ts @@ -1,11 +1,16 @@ -import { TestEnv, assertSentryEvent } from '../../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; -test('should set extras from multiple consecutive calls', async () => { - const env = await TestEnv.init(__dirname); - const envelope = await env.getEnvelopeRequest(); +afterAll(() => { + cleanupChildProcesses(); +}); - assertSentryEvent(envelope[2], { - message: 'consecutive_calls', - extra: { extra: [], Infinity: 2, null: 0, obj: { foo: ['bar', 'baz', 1] } }, - }); +test('should set extras from multiple consecutive calls', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'consecutive_calls', + extra: { extra: [], Infinity: 2, null: 0, obj: { foo: ['bar', 'baz', 1] } }, + }, + }) + .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/setExtras/multiple-extras/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setExtras/multiple-extras/scenario.ts index a8fe79a25291..925140258972 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setExtras/multiple-extras/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setExtras/multiple-extras/scenario.ts @@ -1,8 +1,10 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + transport: loggingTransport, }); Sentry.setExtras({ diff --git a/dev-packages/node-integration-tests/suites/public-api/setExtras/multiple-extras/test.ts b/dev-packages/node-integration-tests/suites/public-api/setExtras/multiple-extras/test.ts index 940330a68a63..c6282f3f25b1 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setExtras/multiple-extras/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setExtras/multiple-extras/test.ts @@ -1,16 +1,21 @@ -import { TestEnv, assertSentryEvent } from '../../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; -test('should record an extras object', async () => { - const env = await TestEnv.init(__dirname); - const event = await env.getEnvelopeRequest(); +afterAll(() => { + cleanupChildProcesses(); +}); - assertSentryEvent(event[2], { - message: 'multiple_extras', - extra: { - extra_1: [1, ['foo'], 'bar'], - extra_2: 'baz', - extra_3: 3.141592653589793, - extra_4: { qux: { quux: false } }, - }, - }); +test('should record an extras object', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'multiple_extras', + extra: { + extra_1: [1, ['foo'], 'bar'], + extra_2: 'baz', + extra_3: 3.141592653589793, + extra_4: { qux: { quux: false } }, + }, + }, + }) + .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/setMeasurement/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setMeasurement/scenario.ts new file mode 100644 index 000000000000..921374380376 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/setMeasurement/scenario.ts @@ -0,0 +1,16 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1, + transport: loggingTransport, +}); + +Sentry.startSpan({ name: 'some_transaction' }, () => { + Sentry.setMeasurement('metric.foo', 42, 'ms'); + Sentry.setMeasurement('metric.bar', 1337, 'nanoseconds'); + Sentry.setMeasurement('metric.baz', 99, 's'); + Sentry.setMeasurement('metric.baz', 1, ''); +}); diff --git a/dev-packages/node-integration-tests/suites/public-api/setMeasurement/test.ts b/dev-packages/node-integration-tests/suites/public-api/setMeasurement/test.ts new file mode 100644 index 000000000000..63a32d270a72 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/setMeasurement/test.ts @@ -0,0 +1,20 @@ +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should attach measurement to transaction', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + transaction: { + transaction: 'some_transaction', + measurements: { + 'metric.foo': { value: 42, unit: 'ms' }, + 'metric.bar': { value: 1337, unit: 'nanoseconds' }, + 'metric.baz': { value: 1, unit: '' }, + }, + }, + }) + .start(done); +}); diff --git a/dev-packages/node-integration-tests/suites/public-api/setTag/with-primitives/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setTag/with-primitives/scenario.ts index 50b094b84bb2..8bb972127dc0 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setTag/with-primitives/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setTag/with-primitives/scenario.ts @@ -1,8 +1,10 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + transport: loggingTransport, }); Sentry.setTag('tag_1', 'foo'); diff --git a/dev-packages/node-integration-tests/suites/public-api/setTag/with-primitives/test.ts b/dev-packages/node-integration-tests/suites/public-api/setTag/with-primitives/test.ts index 623502d751b6..aaa81e79c705 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setTag/with-primitives/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setTag/with-primitives/test.ts @@ -1,17 +1,22 @@ -import { TestEnv, assertSentryEvent } from '../../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; -test('should set primitive tags', async () => { - const env = await TestEnv.init(__dirname); - const event = await env.getEnvelopeRequest(); +afterAll(() => { + cleanupChildProcesses(); +}); - assertSentryEvent(event[2], { - message: 'primitive_tags', - tags: { - tag_1: 'foo', - tag_2: 3.141592653589793, - tag_3: false, - tag_4: null, - tag_6: -1, - }, - }); +test('should set primitive tags', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'primitive_tags', + tags: { + tag_1: 'foo', + tag_2: 3.141592653589793, + tag_3: false, + tag_4: null, + tag_6: -1, + }, + }, + }) + .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/setTags/with-primitives/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setTags/with-primitives/scenario.ts index 50b094b84bb2..8bb972127dc0 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setTags/with-primitives/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setTags/with-primitives/scenario.ts @@ -1,8 +1,10 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + transport: loggingTransport, }); Sentry.setTag('tag_1', 'foo'); diff --git a/dev-packages/node-integration-tests/suites/public-api/setTags/with-primitives/test.ts b/dev-packages/node-integration-tests/suites/public-api/setTags/with-primitives/test.ts index 623502d751b6..aaa81e79c705 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setTags/with-primitives/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setTags/with-primitives/test.ts @@ -1,17 +1,22 @@ -import { TestEnv, assertSentryEvent } from '../../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; -test('should set primitive tags', async () => { - const env = await TestEnv.init(__dirname); - const event = await env.getEnvelopeRequest(); +afterAll(() => { + cleanupChildProcesses(); +}); - assertSentryEvent(event[2], { - message: 'primitive_tags', - tags: { - tag_1: 'foo', - tag_2: 3.141592653589793, - tag_3: false, - tag_4: null, - tag_6: -1, - }, - }); +test('should set primitive tags', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'primitive_tags', + tags: { + tag_1: 'foo', + tag_2: 3.141592653589793, + tag_3: false, + tag_4: null, + tag_6: -1, + }, + }, + }) + .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/setUser/unset_user/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setUser/unset_user/scenario.ts index 6772164a0cc7..42df2033bb33 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setUser/unset_user/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setUser/unset_user/scenario.ts @@ -1,8 +1,10 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + transport: loggingTransport, }); Sentry.captureMessage('no_user'); diff --git a/dev-packages/node-integration-tests/suites/public-api/setUser/unset_user/test.ts b/dev-packages/node-integration-tests/suites/public-api/setUser/unset_user/test.ts index 9b95e30f68cc..a8c5f4483da3 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setUser/unset_user/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setUser/unset_user/test.ts @@ -1,29 +1,22 @@ -import type { Event } from '@sentry/node'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; -import { TestEnv, assertSentryEvent } from '../../../../utils'; - -test('should unset user', async () => { - const env = await TestEnv.init(__dirname); - const events = await env.getMultipleEnvelopeRequest({ count: 3 }); - - assertSentryEvent(events[0][2], { - message: 'no_user', - }); - - expect((events[0] as Event).user).not.toBeDefined(); - - assertSentryEvent(events[1][2], { - message: 'user', - user: { - id: 'foo', - ip_address: 'bar', - other_key: 'baz', - }, - }); - - assertSentryEvent(events[2][2], { - message: 'unset_user', - }); +afterAll(() => { + cleanupChildProcesses(); +}); - expect((events[2] as Event).user).not.toBeDefined(); +test('should unset user', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ event: { message: 'no_user' } }) + .expect({ + event: { + message: 'user', + user: { + id: 'foo', + ip_address: 'bar', + other_key: 'baz', + }, + }, + }) + .expect({ event: { message: 'unset_user' } }) + .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/setUser/update_user/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setUser/update_user/scenario.ts index 089f6ceeac1d..001651461ab6 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setUser/update_user/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setUser/update_user/scenario.ts @@ -1,8 +1,10 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + transport: loggingTransport, }); Sentry.setUser({ diff --git a/dev-packages/node-integration-tests/suites/public-api/setUser/update_user/test.ts b/dev-packages/node-integration-tests/suites/public-api/setUser/update_user/test.ts index 3f630bdf4586..8f24c3d93949 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setUser/update_user/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setUser/update_user/test.ts @@ -1,21 +1,27 @@ -import { TestEnv, assertSentryEvent } from '../../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; -test('should update user', async () => { - const env = await TestEnv.init(__dirname); - const envelopes = await env.getMultipleEnvelopeRequest({ count: 2 }); - - assertSentryEvent(envelopes[0][2], { - message: 'first_user', - user: { - id: 'foo', - ip_address: 'bar', - }, - }); +afterAll(() => { + cleanupChildProcesses(); +}); - assertSentryEvent(envelopes[1][2], { - message: 'second_user', - user: { - id: 'baz', - }, - }); +test('should update user', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'first_user', + user: { + id: 'foo', + ip_address: 'bar', + }, + }, + }) + .expect({ + event: { + message: 'second_user', + user: { + id: 'baz', + }, + }, + }) + .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage/scenario.ts index a6889bb46a9a..625c1377ea42 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage/scenario.ts @@ -1,9 +1,11 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, + transport: loggingTransport, }); Sentry.startSpan({ name: 'test_span' }, () => undefined); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage/test.ts index 87c0ffc5dad9..86b3bf6d9d22 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage/test.ts @@ -1,10 +1,11 @@ -import { TestEnv, assertSentryTransaction } from '../../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; -test('should send a manually started root span', async () => { - const env = await TestEnv.init(__dirname); - const envelope = await env.getEnvelopeRequest({ envelopeType: 'transaction' }); +afterAll(() => { + cleanupChildProcesses(); +}); - assertSentryTransaction(envelope[2], { - transaction: 'test_span', - }); +test('should send a manually started root span', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ transaction: { transaction: 'test_span' } }) + .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/with-nested-spans/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/with-nested-spans/scenario.ts index 0d33cc197575..d046a076d0b1 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startSpan/with-nested-spans/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/with-nested-spans/scenario.ts @@ -1,9 +1,11 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, + transport: loggingTransport, }); Sentry.startSpan({ name: 'root_span' }, () => { diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/with-nested-spans/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/with-nested-spans/test.ts index 77b83661b8a8..e5ebd5bf9abc 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startSpan/with-nested-spans/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/with-nested-spans/test.ts @@ -1,36 +1,42 @@ -import { TestEnv, assertSentryTransaction } from '../../../../utils'; +import type { SpanJSON } from '@sentry/types'; +import { assertSentryTransaction, cleanupChildProcesses, createRunner } from '../../../../utils/runner'; -test('should report finished spans as children of the root transaction.', async () => { - const env = await TestEnv.init(__dirname); - const envelope = await env.getEnvelopeRequest({ envelopeType: 'transaction' }); - - expect(envelope).toHaveLength(3); +afterAll(() => { + cleanupChildProcesses(); +}); - const rootSpanId = (envelope?.[2] as any)?.contexts?.trace?.span_id; - const span3Id = (envelope?.[2] as any)?.spans?.[1].span_id; +test('should report finished spans as children of the root transaction.', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + transaction: transaction => { + const rootSpanId = transaction.contexts?.trace?.span_id; + const span3Id = transaction.spans?.[1].span_id; - expect(rootSpanId).toEqual(expect.any(String)); - expect(span3Id).toEqual(expect.any(String)); + expect(rootSpanId).toEqual(expect.any(String)); + expect(span3Id).toEqual(expect.any(String)); - assertSentryTransaction(envelope[2], { - transaction: 'root_span', - spans: [ - { - description: 'span_1', - data: { - foo: 'bar', - baz: [1, 2, 3], - }, - parent_span_id: rootSpanId, - }, - { - description: 'span_3', - parent_span_id: rootSpanId, - }, - { - description: 'span_5', - parent_span_id: span3Id, + assertSentryTransaction(transaction, { + transaction: 'root_span', + spans: [ + { + description: 'span_1', + data: { + foo: 'bar', + baz: [1, 2, 3], + }, + parent_span_id: rootSpanId, + }, + { + description: 'span_3', + parent_span_id: rootSpanId, + }, + { + description: 'span_5', + parent_span_id: span3Id, + }, + ] as SpanJSON[], + }); }, - ], - }); + }) + .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/withScope/nested-scopes/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/withScope/nested-scopes/scenario.ts index 47f157ea20db..58bc4efbbac2 100644 --- a/dev-packages/node-integration-tests/suites/public-api/withScope/nested-scopes/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/withScope/nested-scopes/scenario.ts @@ -1,8 +1,10 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + transport: loggingTransport, }); Sentry.setUser({ id: 'qux' }); diff --git a/dev-packages/node-integration-tests/suites/public-api/withScope/nested-scopes/test.ts b/dev-packages/node-integration-tests/suites/public-api/withScope/nested-scopes/test.ts index 63752853046b..fa7384c319b8 100644 --- a/dev-packages/node-integration-tests/suites/public-api/withScope/nested-scopes/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/withScope/nested-scopes/test.ts @@ -1,52 +1,57 @@ -import type { Event } from '@sentry/node'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; -import { TestEnv, assertSentryEvent } from '../../../../utils'; - -test('should allow nested scoping', async () => { - const env = await TestEnv.init(__dirname); - const events = await env.getMultipleEnvelopeRequest({ count: 5 }); - - assertSentryEvent(events[0][2], { - message: 'root_before', - user: { - id: 'qux', - }, - }); - - assertSentryEvent(events[1][2], { - message: 'outer_before', - user: { - id: 'qux', - }, - tags: { - foo: false, - }, - }); - - assertSentryEvent(events[2][2], { - message: 'inner', - tags: { - foo: false, - bar: 10, - }, - }); - - expect((events[2] as Event).user).toBeUndefined(); - - assertSentryEvent(events[3][2], { - message: 'outer_after', - user: { - id: 'baz', - }, - tags: { - foo: false, - }, - }); +afterAll(() => { + cleanupChildProcesses(); +}); - assertSentryEvent(events[4][2], { - message: 'root_after', - user: { - id: 'qux', - }, - }); +test('should allow nested scoping', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'root_before', + user: { + id: 'qux', + }, + }, + }) + .expect({ + event: { + message: 'outer_before', + user: { + id: 'qux', + }, + tags: { + foo: false, + }, + }, + }) + .expect({ + event: { + message: 'inner', + tags: { + foo: false, + bar: 10, + }, + }, + }) + .expect({ + event: { + message: 'outer_after', + user: { + id: 'baz', + }, + tags: { + foo: false, + }, + }, + }) + .expect({ + event: { + message: 'root_after', + user: { + id: 'qux', + }, + }, + }) + .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts b/dev-packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts index 6e8766e8ff3e..a94d7c46dba0 100644 --- a/dev-packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts +++ b/dev-packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts @@ -1,45 +1,34 @@ -import path from 'path'; -import nock from 'nock'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; -import { TestEnv } from '../../../utils'; +afterEach(() => { + cleanupChildProcesses(); +}); test('should aggregate successful and crashed sessions', async () => { - const env = await TestEnv.init(__dirname, `${path.resolve(__dirname, '..')}/server.ts`); - - const envelope = ( - await Promise.race([ - env.getMultipleEnvelopeRequest({ url: `${env.url}/success`, endServer: false, envelopeType: 'sessions' }), - env.getMultipleEnvelopeRequest({ url: `${env.url}/error_unhandled`, endServer: false, envelopeType: 'sessions' }), - env.getMultipleEnvelopeRequest({ - url: `${env.url}/success_next`, - endServer: false, - envelopeType: 'sessions', - }), - ]) - )[0]; - - nock.cleanAll(); - env.server.close(); - - expect(envelope[0]).toMatchObject({ - sent_at: expect.any(String), - sdk: { - name: 'sentry.javascript.node', - version: expect.any(String), - }, + let _done: undefined | (() => void); + const promise = new Promise(resolve => { + _done = resolve; }); - expect(envelope[1]).toMatchObject({ - type: 'sessions', - }); - - expect(envelope[2]).toMatchObject({ - aggregates: [ - { - started: expect.any(String), - exited: 2, - crashed: 1, + const runner = createRunner(__dirname, 'server.ts') + .ignore('transaction', 'event', 'session') + .expectError() + .expect({ + sessions: { + aggregates: [ + { + started: expect.any(String), + exited: 2, + crashed: 1, + }, + ], }, - ], - }); + }) + .start(_done); + + runner.makeRequest('get', '/success'); + runner.makeRequest('get', '/error_unhandled'); + runner.makeRequest('get', '/success_next'); + + await promise; }); diff --git a/dev-packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts b/dev-packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts index 0112da40d1f4..da9733690507 100644 --- a/dev-packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts +++ b/dev-packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts @@ -1,38 +1,35 @@ -import path from 'path'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; -import { TestEnv } from '../../../utils'; +afterEach(() => { + cleanupChildProcesses(); +}); test('should aggregate successful, crashed and erroneous sessions', async () => { - const env = await TestEnv.init(__dirname, `${path.resolve(__dirname, '..')}/server.ts`); - - const aggregateSessionEnvelope = await Promise.race([ - env.getEnvelopeRequest({ url: `${env.url}/success`, endServer: false, envelopeType: 'sessions' }), - env.getEnvelopeRequest({ url: `${env.url}/error_handled`, endServer: false, envelopeType: 'sessions' }), - env.getEnvelopeRequest({ url: `${env.url}/error_unhandled`, endServer: false, envelopeType: 'sessions' }), - ]); - - await new Promise(resolve => env.server.close(resolve)); - - expect(aggregateSessionEnvelope[0]).toMatchObject({ - sent_at: expect.any(String), - sdk: { - name: 'sentry.javascript.node', - version: expect.any(String), - }, + let _done: undefined | (() => void); + const promise = new Promise(resolve => { + _done = resolve; }); - expect(aggregateSessionEnvelope[1]).toMatchObject({ - type: 'sessions', - }); - - expect(aggregateSessionEnvelope[2]).toMatchObject({ - aggregates: [ - { - started: expect.any(String), - exited: 1, - crashed: 1, - errored: 1, + const runner = createRunner(__dirname, 'server.ts') + .ignore('transaction', 'event', 'session') + .expectError() + .expect({ + sessions: { + aggregates: [ + { + started: expect.any(String), + exited: 1, + crashed: 1, + errored: 1, + }, + ], }, - ], - }); + }) + .start(_done); + + runner.makeRequest('get', '/success'); + runner.makeRequest('get', '/error_handled'); + runner.makeRequest('get', '/error_unhandled'); + + await promise; }); diff --git a/dev-packages/node-integration-tests/suites/sessions/exited-session-aggregate/test.ts b/dev-packages/node-integration-tests/suites/sessions/exited-session-aggregate/test.ts index ae0182fde295..ba42536a3bbf 100644 --- a/dev-packages/node-integration-tests/suites/sessions/exited-session-aggregate/test.ts +++ b/dev-packages/node-integration-tests/suites/sessions/exited-session-aggregate/test.ts @@ -1,39 +1,33 @@ -import path from 'path'; -import nock from 'nock'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; -import { TestEnv } from '../../../utils'; +afterEach(() => { + cleanupChildProcesses(); +}); test('should aggregate successful sessions', async () => { - const env = await TestEnv.init(__dirname, `${path.resolve(__dirname, '..')}/server.ts`); - - const envelope = await Promise.race([ - env.getEnvelopeRequest({ url: `${env.url}/success`, endServer: false, envelopeType: 'sessions' }), - env.getEnvelopeRequest({ url: `${env.url}/success_next`, endServer: false, envelopeType: 'sessions' }), - env.getEnvelopeRequest({ url: `${env.url}/success_slow`, endServer: false, envelopeType: 'sessions' }), - ]); - - nock.cleanAll(); - env.server.close(); - - expect(envelope).toHaveLength(3); - expect(envelope[0]).toMatchObject({ - sent_at: expect.any(String), - sdk: { - name: 'sentry.javascript.node', - version: expect.any(String), - }, + let _done: undefined | (() => void); + const promise = new Promise(resolve => { + _done = resolve; }); - expect(envelope[1]).toMatchObject({ - type: 'sessions', - }); - - expect(envelope[2]).toMatchObject({ - aggregates: [ - { - started: expect.any(String), - exited: 3, + const runner = createRunner(__dirname, 'server.ts') + .ignore('transaction', 'event', 'session') + .expectError() + .expect({ + sessions: { + aggregates: [ + { + started: expect.any(String), + exited: 3, + }, + ], }, - ], - }); + }) + .start(_done); + + runner.makeRequest('get', '/success'); + runner.makeRequest('get', '/success_next'); + runner.makeRequest('get', '/success_slow'); + + await promise; }); diff --git a/dev-packages/node-integration-tests/suites/sessions/server.ts b/dev-packages/node-integration-tests/suites/sessions/server.ts index e2d2e23ccd4a..e06f00ef486a 100644 --- a/dev-packages/node-integration-tests/suites/sessions/server.ts +++ b/dev-packages/node-integration-tests/suites/sessions/server.ts @@ -1,16 +1,14 @@ -/* eslint-disable no-console */ import type { SessionFlusher } from '@sentry/core'; -import * as Sentry from '@sentry/node-experimental'; -import express from 'express'; - -const app = express(); +import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', }); -app.use(Sentry.Handlers.requestHandler()); +import express from 'express'; + +const app = express(); // ### Taken from manual tests ### // Hack that resets the 60s default flush interval, and replaces it with just a one second interval @@ -52,6 +50,6 @@ app.get('/test/error_handled', (_req, res) => { res.send('Crash!'); }); -app.use(Sentry.Handlers.errorHandler()); +Sentry.setupExpressErrorHandler(app); export default app; diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/scenario-mutation.js b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/scenario-mutation.js similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/scenario-mutation.js rename to dev-packages/node-integration-tests/suites/tracing/apollo-graphql/scenario-mutation.js diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/scenario-query.js b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/scenario-query.js similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/scenario-query.js rename to dev-packages/node-integration-tests/suites/tracing/apollo-graphql/scenario-query.js diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/test.ts b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/test.ts similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/test.ts rename to dev-packages/node-integration-tests/suites/tracing/apollo-graphql/test.ts diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/hapi/scenario.js b/dev-packages/node-integration-tests/suites/tracing/hapi/scenario.js similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing-experimental/hapi/scenario.js rename to dev-packages/node-integration-tests/suites/tracing/hapi/scenario.js diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/hapi/test.ts b/dev-packages/node-integration-tests/suites/tracing/hapi/test.ts similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing-experimental/hapi/test.ts rename to dev-packages/node-integration-tests/suites/tracing/hapi/test.ts diff --git a/dev-packages/node-integration-tests/suites/tracing/metric-summaries/scenario.js b/dev-packages/node-integration-tests/suites/tracing/metric-summaries/scenario.js index 4a0a3ec792e5..422fa4c504a5 100644 --- a/dev-packages/node-integration-tests/suites/tracing/metric-summaries/scenario.js +++ b/dev-packages/node-integration-tests/suites/tracing/metric-summaries/scenario.js @@ -1,5 +1,5 @@ const { loggingTransport } = require('@sentry-internal/node-integration-tests'); -const Sentry = require('@sentry/node-experimental'); +const Sentry = require('@sentry/node'); Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', @@ -8,9 +8,6 @@ Sentry.init({ transport: loggingTransport, }); -// Stop the process from exiting before the transaction is sent -setInterval(() => {}, 1000); - Sentry.startSpan( { name: 'Test Transaction', diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/mongodb/scenario.js b/dev-packages/node-integration-tests/suites/tracing/mongodb/scenario.js similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing-experimental/mongodb/scenario.js rename to dev-packages/node-integration-tests/suites/tracing/mongodb/scenario.js diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/mongodb/test.ts b/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing-experimental/mongodb/test.ts rename to dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/mongoose/scenario.js b/dev-packages/node-integration-tests/suites/tracing/mongoose/scenario.js similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing-experimental/mongoose/scenario.js rename to dev-packages/node-integration-tests/suites/tracing/mongoose/scenario.js diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/mongoose/test.ts b/dev-packages/node-integration-tests/suites/tracing/mongoose/test.ts similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing-experimental/mongoose/test.ts rename to dev-packages/node-integration-tests/suites/tracing/mongoose/test.ts diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/mysql/scenario-withConnect.js b/dev-packages/node-integration-tests/suites/tracing/mysql/scenario-withConnect.js similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing-experimental/mysql/scenario-withConnect.js rename to dev-packages/node-integration-tests/suites/tracing/mysql/scenario-withConnect.js diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/mysql/scenario-withoutCallback.js b/dev-packages/node-integration-tests/suites/tracing/mysql/scenario-withoutCallback.js similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing-experimental/mysql/scenario-withoutCallback.js rename to dev-packages/node-integration-tests/suites/tracing/mysql/scenario-withoutCallback.js diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/mysql/scenario-withoutConnect.js b/dev-packages/node-integration-tests/suites/tracing/mysql/scenario-withoutConnect.js similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing-experimental/mysql/scenario-withoutConnect.js rename to dev-packages/node-integration-tests/suites/tracing/mysql/scenario-withoutConnect.js diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/mysql/test.ts b/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing-experimental/mysql/test.ts rename to dev-packages/node-integration-tests/suites/tracing/mysql/test.ts diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing/mysql2/docker-compose.yml similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/docker-compose.yml rename to dev-packages/node-integration-tests/suites/tracing/mysql2/docker-compose.yml diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/scenario.js b/dev-packages/node-integration-tests/suites/tracing/mysql2/scenario.js similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/scenario.js rename to dev-packages/node-integration-tests/suites/tracing/mysql2/scenario.js diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/test.ts b/dev-packages/node-integration-tests/suites/tracing/mysql2/test.ts similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/test.ts rename to dev-packages/node-integration-tests/suites/tracing/mysql2/test.ts diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/nestjs/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/nestjs/scenario.ts similarity index 92% rename from dev-packages/node-integration-tests/suites/tracing-experimental/nestjs/scenario.ts rename to dev-packages/node-integration-tests/suites/tracing/nestjs/scenario.ts index f0a97953b2d9..19ec6c04c3e3 100644 --- a/dev-packages/node-integration-tests/suites/tracing-experimental/nestjs/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/nestjs/scenario.ts @@ -1,3 +1,5 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck These are only tests /* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/explicit-member-accessibility */ import { loggingTransport, sendPortToRunner } from '@sentry-internal/node-integration-tests'; diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/nestjs/test.ts b/dev-packages/node-integration-tests/suites/tracing/nestjs/test.ts similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing-experimental/nestjs/test.ts rename to dev-packages/node-integration-tests/suites/tracing/nestjs/test.ts diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/nestjs/tsconfig.json b/dev-packages/node-integration-tests/suites/tracing/nestjs/tsconfig.json similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing-experimental/nestjs/tsconfig.json rename to dev-packages/node-integration-tests/suites/tracing/nestjs/tsconfig.json diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/postgres/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing/postgres/docker-compose.yml similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing-experimental/postgres/docker-compose.yml rename to dev-packages/node-integration-tests/suites/tracing/postgres/docker-compose.yml diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/postgres/scenario.js b/dev-packages/node-integration-tests/suites/tracing/postgres/scenario.js similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing-experimental/postgres/scenario.js rename to dev-packages/node-integration-tests/suites/tracing/postgres/scenario.js diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/postgres/test.ts b/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing-experimental/postgres/test.ts rename to dev-packages/node-integration-tests/suites/tracing/postgres/test.ts diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/prisma-orm/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/docker-compose.yml similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing-experimental/prisma-orm/docker-compose.yml rename to dev-packages/node-integration-tests/suites/tracing/prisma-orm/docker-compose.yml diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/prisma-orm/package.json b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/package.json similarity index 92% rename from dev-packages/node-integration-tests/suites/tracing-experimental/prisma-orm/package.json rename to dev-packages/node-integration-tests/suites/tracing/prisma-orm/package.json index b9a5e7998269..b40c92b4356e 100644 --- a/dev-packages/node-integration-tests/suites/tracing-experimental/prisma-orm/package.json +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/package.json @@ -7,7 +7,7 @@ "node": ">=16" }, "scripts": { - "db-up": "docker-compose up -d", + "db-up": "docker compose up -d", "generate": "prisma generate", "migrate": "prisma migrate dev -n sentry-test", "setup": "run-s --silent db-up generate migrate" diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/prisma-orm/prisma/migrations/migration_lock.toml b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/prisma/migrations/migration_lock.toml similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing-experimental/prisma-orm/prisma/migrations/migration_lock.toml rename to dev-packages/node-integration-tests/suites/tracing/prisma-orm/prisma/migrations/migration_lock.toml diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/prisma-orm/prisma/migrations/sentry_test/migration.sql b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/prisma/migrations/sentry_test/migration.sql similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing-experimental/prisma-orm/prisma/migrations/sentry_test/migration.sql rename to dev-packages/node-integration-tests/suites/tracing/prisma-orm/prisma/migrations/sentry_test/migration.sql diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/prisma-orm/prisma/schema.prisma b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/prisma/schema.prisma similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing-experimental/prisma-orm/prisma/schema.prisma rename to dev-packages/node-integration-tests/suites/tracing/prisma-orm/prisma/schema.prisma diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/prisma-orm/scenario.js b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario.js similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing-experimental/prisma-orm/scenario.js rename to dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario.js diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/prisma-orm/setup.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/setup.ts similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing-experimental/prisma-orm/setup.ts rename to dev-packages/node-integration-tests/suites/tracing/prisma-orm/setup.ts diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/prisma-orm/test.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/test.ts similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing-experimental/prisma-orm/test.ts rename to dev-packages/node-integration-tests/suites/tracing/prisma-orm/test.ts diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/prisma-orm/yarn.lock b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/yarn.lock similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing-experimental/prisma-orm/yarn.lock rename to dev-packages/node-integration-tests/suites/tracing/prisma-orm/yarn.lock diff --git a/dev-packages/node-integration-tests/suites/tracing/spans/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/spans/scenario.ts index 1a3faae3805c..5220701b1b4d 100644 --- a/dev-packages/node-integration-tests/suites/tracing/spans/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/spans/scenario.ts @@ -1,17 +1,32 @@ -import * as http from 'http'; -import * as Sentry from '@sentry/node-experimental'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, + transport: loggingTransport, }); +import * as http from 'http'; + // eslint-disable-next-line @typescript-eslint/no-floating-promises Sentry.startSpan({ name: 'test_transaction' }, async () => { - http.get('http://match-this-url.com/api/v0'); - http.get('http://match-this-url.com/api/v1'); - - // Give it a tick to resolve... - await new Promise(resolve => setTimeout(resolve, 100)); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v0`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v1`); }); + +function makeHttpRequest(url: string): Promise { + return new Promise(resolve => { + http + .request(url, httpRes => { + httpRes.on('data', () => { + // we don't care about data + }); + httpRes.on('end', () => { + resolve(); + }); + }) + .end(); + }); +} diff --git a/dev-packages/node-integration-tests/suites/tracing/spans/test.ts b/dev-packages/node-integration-tests/suites/tracing/spans/test.ts index 0d882dd84b31..45d79eb6f23b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/spans/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/spans/test.ts @@ -1,34 +1,48 @@ -import nock from 'nock'; +import { createRunner } from '../../../utils/runner'; +import { createTestServer } from '../../../utils/server'; -import { TestEnv, assertSentryTransaction } from '../../../utils'; +test('should capture spans for outgoing http requests', done => { + expect.assertions(3); -test('should capture spans for outgoing http requests', async () => { - const match1 = nock('http://match-this-url.com').get('/api/v0').reply(200); - const match2 = nock('http://match-this-url.com').get('/api/v1').reply(200); - - const env = await TestEnv.init(__dirname); - const envelope = await env.getEnvelopeRequest({ envelopeType: 'transaction' }); - - expect(match1.isDone()).toBe(true); - expect(match2.isDone()).toBe(true); - - expect(envelope).toHaveLength(3); - - assertSentryTransaction(envelope[2], { - transaction: 'test_transaction', - spans: [ - { - description: 'GET http://match-this-url.com/api/v0', - op: 'http.client', - origin: 'auto.http.node.http', - status: 'ok', - }, - { - description: 'GET http://match-this-url.com/api/v1', - op: 'http.client', - origin: 'auto.http.node.http', - status: 'ok', + createTestServer(done) + .get('/api/v0', () => { + // Just ensure we're called + expect(true).toBe(true); + }) + .get( + '/api/v1', + () => { + // Just ensure we're called + expect(true).toBe(true); }, - ], - }); + 404, + ) + .start() + .then(SERVER_URL => { + createRunner(__dirname, 'scenario.ts') + .withEnv({ SERVER_URL }) + .expect({ + transaction: { + transaction: 'test_transaction', + spans: expect.arrayContaining([ + expect.objectContaining({ + description: expect.stringMatching(/GET .*\/api\/v0/), + op: 'http.client', + origin: 'auto.http.otel.http', + status: 'ok', + }), + expect.objectContaining({ + description: expect.stringMatching(/GET .*\/api\/v1/), + op: 'http.client', + origin: 'auto.http.otel.http', + status: 'unknown_error', + data: expect.objectContaining({ + 'http.response.status_code': 404, + }), + }), + ]), + }, + }) + .start(done); + }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts index 9d8da27fa7b7..c346b617b9e6 100644 --- a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts @@ -1,17 +1,20 @@ -import * as http from 'http'; -import * as Sentry from '@sentry/node-experimental'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, tracePropagationTargets: [/\/v0/, 'v1'], - integrations: [Sentry.httpIntegration({ tracing: true })], + integrations: [], + transport: loggingTransport, }); +import * as http from 'http'; + Sentry.startSpan({ name: 'test_span' }, () => { - 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'); + http.get(`${process.env.SERVER_URL}/api/v0`); + http.get(`${process.env.SERVER_URL}/api/v1`); + http.get(`${process.env.SERVER_URL}/api/v2`); + http.get(`${process.env.SERVER_URL}/api/v3`); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/test.ts b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/test.ts index 01b75ab10330..e87e9f3df1bc 100644 --- a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/test.ts @@ -1,42 +1,35 @@ -import nock from 'nock'; - -import { TestEnv, runScenario } from '../../../utils'; - -test('HttpIntegration should instrument correct requests when tracePropagationTargets option is provided', async () => { - const match1 = nock('http://match-this-url.com') - .get('/api/v0') - .matchHeader('baggage', val => typeof val === 'string') - .matchHeader('sentry-trace', val => typeof val === 'string') - .reply(200); - - const match2 = nock('http://match-this-url.com') - .get('/api/v1') - .matchHeader('baggage', val => typeof val === 'string') - .matchHeader('sentry-trace', val => typeof val === 'string') - .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); +import { createRunner } from '../../../utils/runner'; +import { createTestServer } from '../../../utils/server'; + +test('HttpIntegration should instrument correct requests when tracePropagationTargets option is provided', done => { + expect.assertions(9); + + createTestServer(done) + .get('/api/v0', headers => { + expect(typeof headers['baggage']).toBe('string'); + expect(typeof headers['sentry-trace']).toBe('string'); + }) + .get('/api/v1', headers => { + expect(typeof headers['baggage']).toBe('string'); + expect(typeof headers['sentry-trace']).toBe('string'); + }) + .get('/api/v2', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .get('/api/v3', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .start() + .then(SERVER_URL => { + createRunner(__dirname, 'scenario.ts') + .withEnv({ SERVER_URL }) + .expect({ + transaction: { + // we're not too concerned with the actual transaction here since this is tested elsewhere + }, + }) + .start(done); + }); }); diff --git a/dev-packages/node-integration-tests/utils/run-tests.ts b/dev-packages/node-integration-tests/utils/run-tests.ts deleted file mode 100644 index 68243884d0a1..000000000000 --- a/dev-packages/node-integration-tests/utils/run-tests.ts +++ /dev/null @@ -1,138 +0,0 @@ -/* eslint-disable no-console */ -import childProcess from 'child_process'; -import os from 'os'; -import path from 'path'; -import yargs from 'yargs'; - -const args = yargs - .option('t', { - alias: 'testNamePattern', - type: 'string', - description: 'Filter for a specific test spec\nsee: https://jestjs.io/docs/cli#--testnamepatternregex', - }) - .option('watch', { - type: 'boolean', - description: 'Run tests in watch mode\nsee: https://jestjs.io/docs/cli#--watch', - }).argv; - -// This variable will act as a job queue that is consumed by a number of worker threads. Each item represents a test to run. -const testPaths = childProcess.execSync('jest --listTests', { encoding: 'utf8' }).trim().split('\n'); - -const numTests = testPaths.length; -const fails: string[] = []; -const skips: string[] = []; - -function getTestPath(testPath: string): string { - const cwd = process.cwd(); - return path.relative(cwd, testPath); -} - -// We're creating a worker for each CPU core. -const workers = os.cpus().map(async () => { - while (testPaths.length > 0) { - const testPath = testPaths.pop() as string; - await new Promise(resolve => { - const jestArgs = ['--runTestsByPath', testPath as string, '--forceExit', '--colors']; - - if (args.t) { - jestArgs.push('-t', args.t); - } - - if (args.watch) { - jestArgs.push('--watch'); - } - - const jestProcess = childProcess.spawn('jest', jestArgs); - - // We're collecting the output and logging it all at once instead of inheriting stdout and stderr, so that - // test outputs of the individual workers aren't interwoven, in case they print at the same time. - let output = ''; - - jestProcess.stdout.on('data', (data: Buffer) => { - output = output + data.toString(); - }); - - jestProcess.stderr.on('data', (data: Buffer) => { - output = output + data.toString(); - }); - - jestProcess.on('error', error => { - console.log(`"${getTestPath(testPath)}" finished with error`, error); - console.log(output); - fails.push(`FAILED: ${getTestPath(testPath)}`); - resolve(); - }); - - jestProcess.on('exit', exitcode => { - const hasError = exitcode !== 0; - const skippedOutput = checkSkippedAllTests(output); - - if (skippedOutput && !hasError) { - skips.push(`SKIPPED: ${getTestPath(testPath)}`); - } else { - console.log(output); - } - - if (hasError) { - fails.push(`FAILED: ${getTestPath(testPath)}`); - } - resolve(); - }); - }); - } -}); - -// eslint-disable-next-line @typescript-eslint/no-floating-promises -Promise.all(workers).then(() => { - console.log('-------------------'); - - const failCount = fails.length; - const skipCount = skips.length; - const totalCount = numTests; - const successCount = numTests - failCount - skipCount; - const nonSkippedCount = totalCount - skipCount; - - if (skips.length) { - console.log('\x1b[2m%s\x1b[0m', '\nSkipped tests:'); - skips.forEach(skip => { - console.log('\x1b[2m%s\x1b[0m', `● ${skip}`); - }); - } - - if (failCount > 0) { - console.log( - '\x1b[31m%s\x1b[0m', - `\n${failCount} of ${nonSkippedCount} tests failed${skipCount ? ` (${skipCount} skipped)` : ''}:\n`, - ); - fails.forEach(fail => { - console.log('\x1b[31m%s\x1b[0m', `● ${fail}`); - }); - process.exit(1); - } else { - console.log( - '\x1b[32m%s\x1b[0m', - `\nSuccessfully ran ${successCount} tests${skipCount ? ` (${skipCount} skipped)` : ''}.`, - ); - console.log('\x1b[32m%s\x1b[0m', 'All tests succeeded.'); - process.exit(0); - } -}); - -/** - * Suppress jest output for test suites where all tests were skipped. - * This only clutters the logs and we can safely print a one-liner instead. - */ -function checkSkippedAllTests(output: string): boolean { - const regex = /(.+)Tests:(.+)\s+(.+?)(\d+) skipped(.+), (\d+) total/gm; - const matches = regex.exec(output); - - if (matches) { - const skipped = Number(matches[4]); - const total = Number(matches[6]); - if (!isNaN(skipped) && !isNaN(total) && total === skipped) { - return true; - } - } - - return false; -} diff --git a/dev-packages/node-integration-tests/utils/runner.ts b/dev-packages/node-integration-tests/utils/runner.ts index a9f3a94d4e3d..eb96059d41c5 100644 --- a/dev-packages/node-integration-tests/utils/runner.ts +++ b/dev-packages/node-integration-tests/utils/runner.ts @@ -1,6 +1,6 @@ import { spawn, spawnSync } from 'child_process'; import { join } from 'path'; -import type { Envelope, EnvelopeItemType, Event, SerializedSession } from '@sentry/types'; +import type { Envelope, EnvelopeItemType, Event, SerializedSession, SessionAggregates } from '@sentry/types'; import axios from 'axios'; import { createBasicSentryServer } from './server'; @@ -113,6 +113,9 @@ type Expected = } | { session: Partial | ((event: SerializedSession) => void); + } + | { + sessions: Partial | ((event: SessionAggregates) => void); }; /** Creates a test runner */ @@ -123,6 +126,7 @@ export function createRunner(...paths: string[]) { const expectedEnvelopes: Expected[] = []; const flags: string[] = []; const ignored: EnvelopeItemType[] = []; + let withEnv: Record = {}; let withSentryServer = false; let dockerOptions: DockerOptions | undefined; let ensureNoErrorOutput = false; @@ -141,6 +145,10 @@ export function createRunner(...paths: string[]) { expectError = true; return this; }, + withEnv: function (env: Record) { + withEnv = env; + return this; + }, withFlags: function (...args: string[]) { flags.push(...args); return this; @@ -260,8 +268,8 @@ export function createRunner(...paths: string[]) { } const env = mockServerPort - ? { ...process.env, SENTRY_DSN: `http://public@localhost:${mockServerPort}/1337` } - : process.env; + ? { ...process.env, ...withEnv, SENTRY_DSN: `http://public@localhost:${mockServerPort}/1337` } + : { ...process.env, ...withEnv }; // eslint-disable-next-line no-console if (process.env.DEBUG) console.log('starting scenario', testPath, flags, env.SENTRY_DSN); diff --git a/dev-packages/node-integration-tests/utils/server.ts b/dev-packages/node-integration-tests/utils/server.ts index 01af9558f0ab..f68f1a9c80d4 100644 --- a/dev-packages/node-integration-tests/utils/server.ts +++ b/dev-packages/node-integration-tests/utils/server.ts @@ -31,3 +31,40 @@ export function createBasicSentryServer(onEnvelope: (env: Envelope) => void): Pr }); }); } + +type HeaderAssertCallback = (headers: Record) => void; + +/** Creates a test server that can be used to check headers */ +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function createTestServer(done: (error: unknown) => void) { + const gets: Array<[string, HeaderAssertCallback, number]> = []; + + return { + get: function (path: string, callback: HeaderAssertCallback, result = 200) { + gets.push([path, callback, result]); + return this; + }, + start: async (): Promise => { + const app = express(); + + for (const [path, callback, result] of gets) { + app.get(path, (req, res) => { + try { + callback(req.headers); + } catch (e) { + done(e); + } + + res.status(result).send(); + }); + } + + return new Promise(resolve => { + const server = app.listen(0, () => { + const address = server.address() as AddressInfo; + resolve(`http://localhost:${address.port}`); + }); + }); + }, + }; +} diff --git a/dev-packages/rollup-utils/.eslintrc.cjs b/dev-packages/rollup-utils/.eslintrc.cjs new file mode 100644 index 000000000000..0939c10d3812 --- /dev/null +++ b/dev-packages/rollup-utils/.eslintrc.cjs @@ -0,0 +1,5 @@ +module.exports = { + extends: ['../../.eslintrc.js'], + ignorePatterns: ['otelLoaderTemplate.js.tmpl'], + sourceType: 'module', +}; diff --git a/dev-packages/rollup-utils/code/otelEsmHooksLoaderTemplate.js b/dev-packages/rollup-utils/code/otelEsmHooksLoaderTemplate.js new file mode 100644 index 000000000000..51c72d9d7192 --- /dev/null +++ b/dev-packages/rollup-utils/code/otelEsmHooksLoaderTemplate.js @@ -0,0 +1,2 @@ +import { getFormat, getSource, load, resolve } from '@opentelemetry/instrumentation/hook.mjs'; +export { getFormat, getSource, load, resolve }; diff --git a/dev-packages/rollup-utils/code/otelEsmRegisterLoaderTemplate.js b/dev-packages/rollup-utils/code/otelEsmRegisterLoaderTemplate.js new file mode 100644 index 000000000000..99c312a20ab4 --- /dev/null +++ b/dev-packages/rollup-utils/code/otelEsmRegisterLoaderTemplate.js @@ -0,0 +1,2 @@ +import { register } from 'module'; +register('@opentelemetry/instrumentation/hook.mjs', import.meta.url); diff --git a/dev-packages/rollup-utils/code/sentryNodeEsmHooksLoaderTemplate.js b/dev-packages/rollup-utils/code/sentryNodeEsmHooksLoaderTemplate.js new file mode 100644 index 000000000000..06fb71a76860 --- /dev/null +++ b/dev-packages/rollup-utils/code/sentryNodeEsmHooksLoaderTemplate.js @@ -0,0 +1 @@ +import '@sentry/node/hook'; diff --git a/dev-packages/rollup-utils/code/sentryNodeEsmRegisterLoaderTemplate.js b/dev-packages/rollup-utils/code/sentryNodeEsmRegisterLoaderTemplate.js new file mode 100644 index 000000000000..5a9fa441f106 --- /dev/null +++ b/dev-packages/rollup-utils/code/sentryNodeEsmRegisterLoaderTemplate.js @@ -0,0 +1,2 @@ +import { getFormat, getSource, load, resolve } from '@sentry/node/register'; +export { getFormat, getSource, load, resolve }; diff --git a/dev-packages/rollup-utils/npmHelpers.mjs b/dev-packages/rollup-utils/npmHelpers.mjs index 86941fc8db29..ed6cc4492756 100644 --- a/dev-packages/rollup-utils/npmHelpers.mjs +++ b/dev-packages/rollup-utils/npmHelpers.mjs @@ -5,12 +5,13 @@ import * as fs from 'fs'; import { builtinModules } from 'module'; import * as path from 'path'; +import { fileURLToPath } from 'url'; import deepMerge from 'deepmerge'; +import { defineConfig } from 'rollup'; import { makeCleanupPlugin, - makeCodeCovPlugin, makeDebugBuildStatementReplacePlugin, makeExtractPolyfillsPlugin, makeNodeResolvePlugin, @@ -21,6 +22,8 @@ import { import { makePackageNodeEsm } from './plugins/make-esm-plugin.mjs'; import { mergePlugins } from './utils.mjs'; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + const packageDotJSON = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), './package.json'), { encoding: 'utf8' })); export function makeBaseNPMConfig(options = {}) { @@ -44,8 +47,6 @@ export function makeBaseNPMConfig(options = {}) { excludeIframe: undefined, }); - const codecovPlugin = makeCodeCovPlugin(); - const defaultBaseConfig = { input: entrypoints, @@ -106,7 +107,6 @@ export function makeBaseNPMConfig(options = {}) { debugBuildStatementReplacePlugin, rrwebBuildPlugin, cleanupPlugin, - codecovPlugin, ], // don't include imported modules from outside the package in the final output @@ -141,3 +141,75 @@ export function makeNPMConfigVariants(baseConfig, options = {}) { return variantSpecificConfigs.map(variant => deepMerge(baseConfig, variant)); } + +/** + * This creates a loader file at the target location as part of the rollup build. + * This loader script can then be used in combination with various Node.js flags (like --import=...) to monkeypatch 3rd party modules. + */ +export function makeOtelLoaders(outputFolder, hookVariant) { + if (hookVariant !== 'otel' && hookVariant !== 'sentry-node') { + throw new Error('hookVariant is neither "otel" nor "sentry-node". Pick one.'); + } + + const expectedRegisterLoaderLocation = `${outputFolder}/register.mjs`; + const foundRegisterLoaderExport = Object.keys(packageDotJSON.exports ?? {}).some(key => { + return packageDotJSON?.exports?.[key]?.import?.default === expectedRegisterLoaderLocation; + }); + if (!foundRegisterLoaderExport) { + throw new Error( + `You used the makeOtelLoaders() rollup utility without specifying the register loader inside \`exports[something].import.default\`. Please add "${expectedRegisterLoaderLocation}" as a value there (maybe check for typos - it needs to be "${expectedRegisterLoaderLocation}" exactly).`, + ); + } + + const expectedHooksLoaderLocation = `${outputFolder}/hook.mjs`; + const foundHookLoaderExport = Object.keys(packageDotJSON.exports ?? {}).some(key => { + return packageDotJSON?.exports?.[key]?.import?.default === expectedHooksLoaderLocation; + }); + if (!foundHookLoaderExport) { + throw new Error( + `You used the makeOtelLoaders() rollup utility without specifying the hook loader inside \`exports[something].import.default\`. Please add "${expectedHooksLoaderLocation}" as a value there (maybe check for typos - it needs to be "${expectedHooksLoaderLocation}" exactly).`, + ); + } + + const requiredDep = hookVariant === 'otel' ? '@opentelemetry/instrumentation' : '@sentry/node'; + const foundImportInTheMiddleDep = Object.keys(packageDotJSON.dependencies ?? {}).some(key => { + return key === requiredDep; + }); + if (!foundImportInTheMiddleDep) { + throw new Error( + `You used the makeOtelLoaders() rollup utility but didn't specify the "${requiredDep}" dependency in ${path.resolve( + process.cwd(), + 'package.json', + )}. Please add it to the dependencies.`, + ); + } + + return defineConfig([ + // register() hook + { + input: path.join( + __dirname, + 'code', + hookVariant === 'otel' ? 'otelEsmRegisterLoaderTemplate.js' : 'sentryNodeEsmRegisterLoaderTemplate.js', + ), + external: ['@opentelemetry/instrumentation/hook.mjs', '@sentry/node/register'], + output: { + format: 'esm', + file: path.join(outputFolder, 'register.mjs'), + }, + }, + // --loader hook + { + input: path.join( + __dirname, + 'code', + hookVariant === 'otel' ? 'otelEsmHooksLoaderTemplate.js' : 'sentryNodeEsmHooksLoaderTemplate.js', + ), + external: ['@opentelemetry/instrumentation/hook.mjs', '@sentry/node/hook'], + output: { + format: 'esm', + file: path.join(outputFolder, 'hook.mjs'), + }, + }, + ]); +} diff --git a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs index 38c7811f3d1a..2404e3fc2d38 100644 --- a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs +++ b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs @@ -121,9 +121,7 @@ export function makeTerserPlugin() { // These are used by instrument.ts in utils for identifying HTML elements & events '_sentryCaptured', '_sentryId', - // For v7 backwards-compatibility we need to access txn._frozenDynamicSamplingContext - // TODO (v8): Remove this reserved word - '_frozenDynamicSamplingContext', + '_frozenDsc', // These are used to keep span & scope relationships '_sentryRootSpan', '_sentryChildSpans', diff --git a/docs/v8-new-performance-apis.md b/docs/v8-new-performance-apis.md index 713e99d5d1f1..614df1100611 100644 --- a/docs/v8-new-performance-apis.md +++ b/docs/v8-new-performance-apis.md @@ -47,10 +47,10 @@ There are three key APIs available to start spans: - `startSpanManual()` - `startInactiveSpan()` -All three span APIs take a `SpanContext` as a first argument, which has the following shape: +All three span APIs take `StartSpanOptions` as a first argument, which has the following shape: ```ts -interface SpanContext { +interface StartSpanOptions { // The only required field - the name of the span name: string; attributes?: SpanAttributes; diff --git a/nx.json b/nx.json index 1fe8820b9110..d4a90a3a6777 100644 --- a/nx.json +++ b/nx.json @@ -10,7 +10,12 @@ }, "namedInputs": { "default": ["{projectRoot}/**/*", "sharedGlobals"], - "sharedGlobals": ["{workspaceRoot}/*.js", "{workspaceRoot}/*.json", "{workspaceRoot}/yarn.lock"], + "sharedGlobals": [ + "{workspaceRoot}/*.js", + "{workspaceRoot}/*.json", + "{workspaceRoot}/yarn.lock", + "{workspaceRoot}/dev-packages/rollup-utils/**" + ], "production": ["default", "!{projectRoot}/test/**/*", "!{projectRoot}/**/*.md", "!{projectRoot}/*.tgz"] }, "targetDefaults": { diff --git a/package.json b/package.json index 5c1f37ca4c33..00945d179e72 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "postpublish": "lerna run --stream --concurrency 1 postpublish", "test": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\" test", "test:unit": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\" test:unit", - "test-ci-browser": "lerna run test --ignore \"@sentry/{bun,deno,node,node-experimental,profiling-node,serverless,google-cloud,nextjs,remix,gatsby,sveltekit,vercel-edge}\" --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\"", + "test-ci-browser": "lerna run test --ignore \"@sentry/{bun,deno,node,profiling-node,serverless,google-cloud,nextjs,remix,gatsby,sveltekit,vercel-edge}\" --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\"", "test-ci-node": "ts-node ./scripts/node-unit-tests.ts", "test-ci-bun": "lerna run test --scope @sentry/bun", "test:update-snapshots": "lerna run test:update-snapshots", @@ -48,6 +48,7 @@ "packages/astro", "packages/aws-serverless", "packages/browser", + "packages/browser-utils", "packages/bun", "packages/core", "packages/deno", @@ -60,7 +61,6 @@ "packages/integration-shims", "packages/nextjs", "packages/node", - "packages/node-experimental", "packages/opentelemetry", "packages/profiling-node", "packages/react", @@ -70,7 +70,6 @@ "packages/replay-worker", "packages/svelte", "packages/sveltekit", - "packages/tracing-internal", "packages/types", "packages/typescript", "packages/utils", @@ -78,9 +77,11 @@ "packages/vue", "packages/wasm", "dev-packages/browser-integration-tests", + "dev-packages/bundle-analyzer-scenarios", "dev-packages/e2e-tests", "dev-packages/node-integration-tests", "dev-packages/overhead-metrics", + "dev-packages/event-proxy-server", "dev-packages/size-limit-gh-action", "dev-packages/rollup-utils" ], diff --git a/packages/angular/src/index.ts b/packages/angular/src/index.ts index fe4c1fd04c38..f0911eb9a440 100644 --- a/packages/angular/src/index.ts +++ b/packages/angular/src/index.ts @@ -2,7 +2,7 @@ export type { ErrorHandlerOptions } from './errorhandler'; export * from '@sentry/browser'; -export { init } from './sdk'; +export { init, getDefaultIntegrations } from './sdk'; export { createErrorHandler, SentryErrorHandler } from './errorhandler'; export { browserTracingIntegration, diff --git a/packages/angular/src/sdk.ts b/packages/angular/src/sdk.ts index e47e06f6233b..344ba91183df 100755 --- a/packages/angular/src/sdk.ts +++ b/packages/angular/src/sdk.ts @@ -1,25 +1,52 @@ import { VERSION } from '@angular/core'; import type { BrowserOptions } from '@sentry/browser'; -import { getDefaultIntegrations, init as browserInit, setContext } from '@sentry/browser'; -import { applySdkMetadata } from '@sentry/core'; +import { + breadcrumbsIntegration, + globalHandlersIntegration, + httpContextIntegration, + linkedErrorsIntegration, +} from '@sentry/browser'; +import { init as browserInit, setContext } from '@sentry/browser'; +import { + applySdkMetadata, + dedupeIntegration, + functionToStringIntegration, + inboundFiltersIntegration, +} from '@sentry/core'; +import type { Integration } from '@sentry/types'; import { logger } from '@sentry/utils'; import { IS_DEBUG_BUILD } from './flags'; +/** + * Get the default integrations for the Angular SDK. + */ +export function getDefaultIntegrations(): Integration[] { + // Don't include the BrowserApiErrors integration as it interferes with the Angular SDK's `ErrorHandler`: + // BrowserApiErrors would catch certain errors before they reach the `ErrorHandler` and + // thus provide a lower fidelity error than what `SentryErrorHandler` + // (see errorhandler.ts) would provide. + // + // see: + // - https://github.com/getsentry/sentry-javascript/issues/5417#issuecomment-1453407097 + // - https://github.com/getsentry/sentry-javascript/issues/2744 + return [ + inboundFiltersIntegration(), + functionToStringIntegration(), + breadcrumbsIntegration(), + globalHandlersIntegration(), + linkedErrorsIntegration(), + dedupeIntegration(), + httpContextIntegration(), + ]; +} + /** * Inits the Angular SDK */ export function init(options: BrowserOptions): void { const opts = { - // Filter out BrowserApiErrors integration as it interferes with our Angular `ErrorHandler`: - // BrowserApiErrors would catch certain errors before they reach the `ErrorHandler` and thus provide a - // lower fidelity error than what `SentryErrorHandler` (see errorhandler.ts) would provide. - // see: - // - https://github.com/getsentry/sentry-javascript/issues/5417#issuecomment-1453407097 - // - https://github.com/getsentry/sentry-javascript/issues/2744 - defaultIntegrations: getDefaultIntegrations(options).filter(integration => { - return integration.name !== 'BrowserApiErrors'; - }), + defaultIntegrations: getDefaultIntegrations(), ...options, }; diff --git a/packages/angular/test/sdk.test.ts b/packages/angular/test/sdk.test.ts index 0f97b4e84026..54756fba72fe 100644 --- a/packages/angular/test/sdk.test.ts +++ b/packages/angular/test/sdk.test.ts @@ -1,6 +1,6 @@ import * as SentryBrowser from '@sentry/browser'; import { vi } from 'vitest'; -import { getDefaultIntegrations, init } from '../src/index'; +import { getDefaultIntegrations, init } from '../src/sdk'; describe('init', () => { it('sets the Angular version (if available) in the global scope', () => { @@ -14,39 +14,16 @@ describe('init', () => { expect(setContextSpy).toHaveBeenCalledWith('angular', { version: 14 }); }); - describe('filtering out the `BrowserApiErrors` integration', () => { - const browserInitSpy = vi.spyOn(SentryBrowser, 'init'); + it('does not include the BrowserApiErrors integration', () => { + const browserDefaultIntegrationsWithoutBrowserApiErrors = SentryBrowser.getDefaultIntegrations() + .filter(i => i.name !== 'BrowserApiErrors') + .map(i => i.name) + .sort(); - beforeEach(() => { - browserInitSpy.mockClear(); - }); + const angularDefaultIntegrations = getDefaultIntegrations() + .map(i => i.name) + .sort(); - it('filters if `defaultIntegrations` is not set', () => { - init({}); - - expect(browserInitSpy).toHaveBeenCalledTimes(1); - - const options = browserInitSpy.mock.calls[0][0] || {}; - expect(options.defaultIntegrations).not.toContainEqual(expect.objectContaining({ name: 'BrowserApiErrors' })); - }); - - it("doesn't filter if `defaultIntegrations` is set to `false`", () => { - init({ defaultIntegrations: false }); - - expect(browserInitSpy).toHaveBeenCalledTimes(1); - - const options = browserInitSpy.mock.calls[0][0] || {}; - expect(options.defaultIntegrations).toEqual(false); - }); - - it("doesn't filter if `defaultIntegrations` is overwritten", () => { - const defaultIntegrations = getDefaultIntegrations({}); - init({ defaultIntegrations }); - - expect(browserInitSpy).toHaveBeenCalledTimes(1); - - const options = browserInitSpy.mock.calls[0][0] || {}; - expect(options.defaultIntegrations).toEqual(defaultIntegrations); - }); + expect(angularDefaultIntegrations).toEqual(browserDefaultIntegrationsWithoutBrowserApiErrors); }); }); diff --git a/packages/astro/package.json b/packages/astro/package.json index 7a08d5fbabf5..d63ec8f5728f 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -21,7 +21,8 @@ "cjs", "esm", "types", - "types-ts3.8" + "types-ts3.8", + "register.mjs" ], "main": "build/cjs/index.client.js", "module": "build/esm/index.server.js", @@ -40,7 +41,17 @@ "import": "./build/esm/integration/middleware/index.js", "require": "./build/cjs/integration/middleware/index.js", "types": "./build/types/integration/middleware/index.types.d.ts" - } + }, + "./register": { + "import": { + "default": "./build/register.mjs" + } + }, +"./hook": { + "import": { + "default": "./build/hook.mjs" + } +} }, "publishConfig": { "access": "public" @@ -58,7 +69,7 @@ }, "devDependencies": { "astro": "^3.5.0", - "vite": "4.0.5" + "vite": "4.5.3" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/astro/rollup.npm.config.mjs b/packages/astro/rollup.npm.config.mjs index c485392d0ec7..ca3b338433a7 100644 --- a/packages/astro/rollup.npm.config.mjs +++ b/packages/astro/rollup.npm.config.mjs @@ -1,4 +1,4 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; +import { makeBaseNPMConfig, makeNPMConfigVariants, makeOtelLoaders } from '@sentry-internal/rollup-utils'; const variants = makeNPMConfigVariants( makeBaseNPMConfig({ @@ -14,4 +14,4 @@ const variants = makeNPMConfigVariants( }), ); -export default variants; +export default [...variants, ...makeOtelLoaders('./build', 'sentry-node')]; diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 1cc715c8345a..7dd2e589b86c 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -82,6 +82,7 @@ export { mysqlIntegration, mysql2Integration, nestIntegration, + setupNestErrorHandler, postgresIntegration, prismaIntegration, hapiIntegration, diff --git a/packages/aws-serverless/package.json b/packages/aws-serverless/package.json index f341f28809f1..7109d496c13a 100644 --- a/packages/aws-serverless/package.json +++ b/packages/aws-serverless/package.json @@ -12,7 +12,8 @@ "files": [ "cjs", "types", - "types-ts3.8" + "types-ts3.8", + "register.mjs" ], "main": "build/npm/cjs/index.js", "types": "build/npm/types/index.d.ts", @@ -23,7 +24,17 @@ "types": "./build/npm/types/index.d.ts", "default": "./build/npm/cjs/index.js" } - } + }, + "./register": { + "import": { + "default": "./build/register.mjs" + } + }, +"./hook": { + "import": { + "default": "./build/hook.mjs" + } +} }, "typesVersions": { "<4.9": { diff --git a/packages/aws-serverless/rollup.npm.config.mjs b/packages/aws-serverless/rollup.npm.config.mjs index ff28359cfeed..59af857e3c0a 100644 --- a/packages/aws-serverless/rollup.npm.config.mjs +++ b/packages/aws-serverless/rollup.npm.config.mjs @@ -1,13 +1,16 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; +import { makeBaseNPMConfig, makeNPMConfigVariants, makeOtelLoaders } from '@sentry-internal/rollup-utils'; -export default makeNPMConfigVariants( - makeBaseNPMConfig({ - // TODO: `awslambda-auto.ts` is a file which the lambda layer uses to automatically init the SDK. Does it need to be - // in the npm package? Is it possible that some people are using it themselves in the same way the layer uses it (in - // which case removing it would be a breaking change)? Should it stay here or not? - entrypoints: ['src/index.ts', 'src/awslambda-auto.ts'], - // packages with bundles have a different build directory structure - hasBundles: true, - }), - { emitEsm: false }, -); +export default [ + ...makeNPMConfigVariants( + makeBaseNPMConfig({ + // TODO: `awslambda-auto.ts` is a file which the lambda layer uses to automatically init the SDK. Does it need to be + // in the npm package? Is it possible that some people are using it themselves in the same way the layer uses it (in + // which case removing it would be a breaking change)? Should it stay here or not? + entrypoints: ['src/index.ts', 'src/awslambda-auto.ts'], + // packages with bundles have a different build directory structure + hasBundles: true, + }), + { emitEsm: false }, + ), + ...makeOtelLoaders('./build', 'sentry-node'), +]; diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 0cc875d29338..68e0a587fb0c 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -76,6 +76,8 @@ export { expressIntegration, expressErrorHandler, setupExpressErrorHandler, + koaIntegration, + setupKoaErrorHandler, fastifyIntegration, graphqlIntegration, mongoIntegration, @@ -83,6 +85,7 @@ export { mysqlIntegration, mysql2Integration, nestIntegration, + setupNestErrorHandler, postgresIntegration, prismaIntegration, hapiIntegration, @@ -90,6 +93,7 @@ export { spotlightIntegration, initOpenTelemetry, spanToJSON, + trpcMiddleware, } from '@sentry/node'; export { diff --git a/packages/tracing-internal/.eslintrc.js b/packages/browser-utils/.eslintrc.js similarity index 57% rename from packages/tracing-internal/.eslintrc.js rename to packages/browser-utils/.eslintrc.js index efa4c594a069..50f4342a74c6 100644 --- a/packages/tracing-internal/.eslintrc.js +++ b/packages/browser-utils/.eslintrc.js @@ -7,5 +7,11 @@ module.exports = { '@sentry-internal/sdk/no-optional-chaining': 'off', }, }, + { + files: ['src/browser/web-vitals/**'], + rules: { + '@typescript-eslint/explicit-function-return-type': 'off', + }, + }, ], }; diff --git a/packages/tracing-internal/LICENSE b/packages/browser-utils/LICENSE similarity index 100% rename from packages/tracing-internal/LICENSE rename to packages/browser-utils/LICENSE diff --git a/packages/browser-utils/README.md b/packages/browser-utils/README.md new file mode 100644 index 000000000000..108f3f3613c7 --- /dev/null +++ b/packages/browser-utils/README.md @@ -0,0 +1,23 @@ +

    + + Sentry + +

    + +# Sentry JavaScript SDK Browser Utilities + +[![npm version](https://img.shields.io/npm/v/@sentry-internal/browser-utils.svg)](https://www.npmjs.com/package/@sentry-internal/browser-utils) +[![npm dm](https://img.shields.io/npm/dm/@sentry-internal/browser-utils.svg)](https://www.npmjs.com/package/@sentry-internal/browser-utils) +[![npm dt](https://img.shields.io/npm/dt/@sentry-internal/browser-utils.svg)](https://www.npmjs.com/package/@sentry-internal/browser-utils) + +## Links + +- [Official SDK Docs](https://docs.sentry.io/quickstart/) +- [TypeDoc](http://getsentry.github.io/sentry-javascript/) + +## General + +Common utilities used by the Sentry JavaScript SDKs. + +Note: This package is only meant to be used internally, and as such is not part of our public API contract and does not +follow semver. diff --git a/packages/node-experimental/jest.config.js b/packages/browser-utils/jest.config.js similarity index 100% rename from packages/node-experimental/jest.config.js rename to packages/browser-utils/jest.config.js diff --git a/packages/tracing-internal/package.json b/packages/browser-utils/package.json similarity index 89% rename from packages/tracing-internal/package.json rename to packages/browser-utils/package.json index 8f02714c7d72..977fc67d9675 100644 --- a/packages/tracing-internal/package.json +++ b/packages/browser-utils/package.json @@ -1,9 +1,9 @@ { - "name": "@sentry-internal/tracing", + "name": "@sentry-internal/browser-utils", "version": "8.0.0-alpha.7", - "description": "Sentry Internal Tracing Package", + "description": "Browser Utilities for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", - "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/tracing-internal", + "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/browser-utils", "author": "Sentry", "license": "MIT", "engines": { @@ -46,9 +46,6 @@ "@sentry/types": "8.0.0-alpha.7", "@sentry/utils": "8.0.0-alpha.7" }, - "devDependencies": { - "@types/express": "^4.17.14" - }, "scripts": { "build": "run-p build:transpile build:types", "build:dev": "yarn build", @@ -61,7 +58,7 @@ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", - "clean": "rimraf build coverage sentry-internal-tracing-*.tgz", + "clean": "rimraf build coverage sentry-internal-browser-utils-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", "test:unit": "jest", diff --git a/packages/browser-utils/rollup.npm.config.mjs b/packages/browser-utils/rollup.npm.config.mjs new file mode 100644 index 000000000000..d28a7a6f54a0 --- /dev/null +++ b/packages/browser-utils/rollup.npm.config.mjs @@ -0,0 +1,17 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; + +export default makeNPMConfigVariants( + makeBaseNPMConfig({ + packageSpecificConfig: { + output: { + // set exports to 'named' or 'auto' so that rollup doesn't warn + exports: 'named', + // set preserveModules to true because we don't want to bundle everything into one file. + preserveModules: + process.env.SENTRY_BUILD_PRESERVE_MODULES === undefined + ? true + : Boolean(process.env.SENTRY_BUILD_PRESERVE_MODULES), + }, + }, + }), +); diff --git a/packages/tracing-internal/src/browser/backgroundtab.ts b/packages/browser-utils/src/browser/backgroundtab.ts similarity index 100% rename from packages/tracing-internal/src/browser/backgroundtab.ts rename to packages/browser-utils/src/browser/backgroundtab.ts diff --git a/packages/tracing-internal/src/browser/browserTracingIntegration.ts b/packages/browser-utils/src/browser/browserTracingIntegration.ts similarity index 95% rename from packages/tracing-internal/src/browser/browserTracingIntegration.ts rename to packages/browser-utils/src/browser/browserTracingIntegration.ts index 713e4f986c2b..5954ca9d4502 100644 --- a/packages/tracing-internal/src/browser/browserTracingIntegration.ts +++ b/packages/browser-utils/src/browser/browserTracingIntegration.ts @@ -8,6 +8,7 @@ import { getActiveSpan, getClient, getCurrentScope, + getIsolationScope, getRootSpan, spanToJSON, startIdleSpan, @@ -15,7 +16,13 @@ import { } from '@sentry/core'; import type { Client, IntegrationFn, StartSpanOptions, TransactionSource } from '@sentry/types'; import type { Span } from '@sentry/types'; -import { addHistoryInstrumentationHandler, browserPerformanceTimeOrigin, getDomElement, logger } from '@sentry/utils'; +import { + addHistoryInstrumentationHandler, + browserPerformanceTimeOrigin, + getDomElement, + logger, + uuid4, +} from '@sentry/utils'; import { DEBUG_BUILD } from '../common/debug-build'; import { registerBackgroundTabDetection } from './backgroundtab'; @@ -107,20 +114,6 @@ export interface BrowserTracingOptions { */ enableHTTPTimings: boolean; - /** - * _metricOptions allows the user to send options to change how metrics are collected. - * - * _metricOptions is currently experimental. - * - * Default: undefined - */ - _metricOptions?: Partial<{ - /** - * @deprecated This property no longer has any effect and will be removed in v8. - */ - _reportAllChanges: boolean; - }>; - /** * _experiments allows the user to send options to define how this integration works. * @@ -205,8 +198,6 @@ export const browserTracingIntegration = ((_options: Partial> = { name: htmlTreeAsString(entry.target), op: `ui.interaction.${entry.name}`, startTime: startTime, @@ -122,7 +158,7 @@ export function startTrackingInteractions(): void { const componentName = getComponentName(entry.target); if (componentName) { - spanOptions.attributes = { 'ui.component_name': componentName }; + spanOptions.attributes['ui.component_name'] = componentName; } const span = startInactiveSpan(spanOptions); @@ -541,7 +577,11 @@ function setResourceEntrySizeData( * ttfb information is added via vendored web vitals library. */ function _addTtfbRequestTimeToMeasurements(_measurements: Measurements): void { - const navEntry = getNavigationEntry() as TTFBMetric['entries'][number]; + const navEntry = getNavigationEntry(); + if (!navEntry) { + return; + } + const { responseStart, requestStart } = navEntry; if (requestStart <= responseStart) { diff --git a/packages/tracing-internal/src/browser/metrics/utils.ts b/packages/browser-utils/src/browser/metrics/utils.ts similarity index 100% rename from packages/tracing-internal/src/browser/metrics/utils.ts rename to packages/browser-utils/src/browser/metrics/utils.ts diff --git a/packages/tracing-internal/src/browser/request.ts b/packages/browser-utils/src/browser/request.ts similarity index 100% rename from packages/tracing-internal/src/browser/request.ts rename to packages/browser-utils/src/browser/request.ts diff --git a/packages/tracing-internal/src/browser/types.ts b/packages/browser-utils/src/browser/types.ts similarity index 100% rename from packages/tracing-internal/src/browser/types.ts rename to packages/browser-utils/src/browser/types.ts diff --git a/packages/tracing-internal/src/browser/web-vitals/README.md b/packages/browser-utils/src/browser/web-vitals/README.md similarity index 91% rename from packages/tracing-internal/src/browser/web-vitals/README.md rename to packages/browser-utils/src/browser/web-vitals/README.md index c87dd69d55b7..653ee22c7ff1 100644 --- a/packages/tracing-internal/src/browser/web-vitals/README.md +++ b/packages/browser-utils/src/browser/web-vitals/README.md @@ -2,10 +2,10 @@ > A modular library for measuring the [Web Vitals](https://web.dev/vitals/) metrics on real users. -This was vendored from: https://github.com/GoogleChrome/web-vitals: v3.0.4 +This was vendored from: https://github.com/GoogleChrome/web-vitals: v3.5.2 The commit SHA used is: -[7f0ed0bfb03c356e348a558a3eda111b498a2a11](https://github.com/GoogleChrome/web-vitals/tree/7f0ed0bfb03c356e348a558a3eda111b498a2a11) +[7b44bea0d5ba6629c5fd34c3a09cc683077871d0](https://github.com/GoogleChrome/web-vitals/tree/7b44bea0d5ba6629c5fd34c3a09cc683077871d0) Current vendored web vitals are: diff --git a/packages/browser-utils/src/browser/web-vitals/getCLS.ts b/packages/browser-utils/src/browser/web-vitals/getCLS.ts new file mode 100644 index 000000000000..f72b0aa309a0 --- /dev/null +++ b/packages/browser-utils/src/browser/web-vitals/getCLS.ts @@ -0,0 +1,110 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { bindReporter } from './lib/bindReporter'; +import { initMetric } from './lib/initMetric'; +import { observe } from './lib/observe'; +import { onHidden } from './lib/onHidden'; +import { runOnce } from './lib/runOnce'; +import { onFCP } from './onFCP'; +import type { CLSMetric, CLSReportCallback, MetricRatingThresholds, ReportOpts } from './types'; + +/** Thresholds for CLS. See https://web.dev/articles/cls#what_is_a_good_cls_score */ +export const CLSThresholds: MetricRatingThresholds = [0.1, 0.25]; + +/** + * Calculates the [CLS](https://web.dev/articles/cls) value for the current page and + * calls the `callback` function once the value is ready to be reported, along + * with all `layout-shift` performance entries that were used in the metric + * value calculation. The reported value is a `double` (corresponding to a + * [layout shift score](https://web.dev/articles/cls#layout_shift_score)). + * + * If the `reportAllChanges` configuration option is set to `true`, the + * `callback` function will be called as soon as the value is initially + * determined as well as any time the value changes throughout the page + * lifespan. + * + * _**Important:** CLS should be continually monitored for changes throughout + * the entire lifespan of a page—including if the user returns to the page after + * it's been hidden/backgrounded. However, since browsers often [will not fire + * additional callbacks once the user has backgrounded a + * page](https://developer.chrome.com/blog/page-lifecycle-api/#advice-hidden), + * `callback` is always called when the page's visibility state changes to + * hidden. As a result, the `callback` function might be called multiple times + * during the same page load._ + */ +export const onCLS = (onReport: CLSReportCallback, opts: ReportOpts = {}): void => { + // Start monitoring FCP so we can only report CLS if FCP is also reported. + // Note: this is done to match the current behavior of CrUX. + onFCP( + runOnce(() => { + const metric = initMetric('CLS', 0); + let report: ReturnType; + + let sessionValue = 0; + let sessionEntries: LayoutShift[] = []; + + const handleEntries = (entries: LayoutShift[]): void => { + entries.forEach(entry => { + // Only count layout shifts without recent user input. + if (!entry.hadRecentInput) { + const firstSessionEntry = sessionEntries[0]; + const lastSessionEntry = sessionEntries[sessionEntries.length - 1]; + + // If the entry occurred less than 1 second after the previous entry + // and less than 5 seconds after the first entry in the session, + // include the entry in the current session. Otherwise, start a new + // session. + if ( + sessionValue && + entry.startTime - lastSessionEntry.startTime < 1000 && + entry.startTime - firstSessionEntry.startTime < 5000 + ) { + sessionValue += entry.value; + sessionEntries.push(entry); + } else { + sessionValue = entry.value; + sessionEntries = [entry]; + } + } + }); + + // If the current session value is larger than the current CLS value, + // update CLS and the entries contributing to it. + if (sessionValue > metric.value) { + metric.value = sessionValue; + metric.entries = sessionEntries; + report(); + } + }; + + const po = observe('layout-shift', handleEntries); + if (po) { + report = bindReporter(onReport, metric, CLSThresholds, opts.reportAllChanges); + + onHidden(() => { + handleEntries(po.takeRecords() as CLSMetric['entries']); + report(true); + }); + + // Queue a task to report (if nothing else triggers a report first). + // This allows CLS to be reported as soon as FCP fires when + // `reportAllChanges` is true. + setTimeout(report, 0); + } + }), + ); +}; diff --git a/packages/browser-utils/src/browser/web-vitals/getFID.ts b/packages/browser-utils/src/browser/web-vitals/getFID.ts new file mode 100644 index 000000000000..f79b388c042d --- /dev/null +++ b/packages/browser-utils/src/browser/web-vitals/getFID.ts @@ -0,0 +1,70 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { bindReporter } from './lib/bindReporter'; +import { getVisibilityWatcher } from './lib/getVisibilityWatcher'; +import { initMetric } from './lib/initMetric'; +import { observe } from './lib/observe'; +import { onHidden } from './lib/onHidden'; +import { runOnce } from './lib/runOnce'; +import { whenActivated } from './lib/whenActivated'; +import type { FIDMetric, FIDReportCallback, MetricRatingThresholds, ReportOpts } from './types'; + +/** Thresholds for FID. See https://web.dev/articles/fid#what_is_a_good_fid_score */ +export const FIDThresholds: MetricRatingThresholds = [100, 300]; + +/** + * Calculates the [FID](https://web.dev/articles/fid) value for the current page and + * calls the `callback` function once the value is ready, along with the + * relevant `first-input` performance entry used to determine the value. The + * reported value is a `DOMHighResTimeStamp`. + * + * _**Important:** since FID is only reported after the user interacts with the + * page, it's possible that it will not be reported for some page loads._ + */ +export const onFID = (onReport: FIDReportCallback, opts: ReportOpts = {}): void => { + whenActivated(() => { + const visibilityWatcher = getVisibilityWatcher(); + const metric = initMetric('FID'); + // eslint-disable-next-line prefer-const + let report: ReturnType; + + const handleEntry = (entry: PerformanceEventTiming) => { + // Only report if the page wasn't hidden prior to the first input. + if (entry.startTime < visibilityWatcher.firstHiddenTime) { + metric.value = entry.processingStart - entry.startTime; + metric.entries.push(entry); + report(true); + } + }; + + const handleEntries = (entries: FIDMetric['entries']) => { + (entries as PerformanceEventTiming[]).forEach(handleEntry); + }; + + const po = observe('first-input', handleEntries); + report = bindReporter(onReport, metric, FIDThresholds, opts.reportAllChanges); + + if (po) { + onHidden( + runOnce(() => { + handleEntries(po.takeRecords() as FIDMetric['entries']); + po.disconnect(); + }), + ); + } + }); +}; diff --git a/packages/tracing-internal/src/browser/web-vitals/getINP.ts b/packages/browser-utils/src/browser/web-vitals/getINP.ts similarity index 57% rename from packages/tracing-internal/src/browser/web-vitals/getINP.ts rename to packages/browser-utils/src/browser/web-vitals/getINP.ts index 546838bff15d..5c4a185aa92f 100644 --- a/packages/tracing-internal/src/browser/web-vitals/getINP.ts +++ b/packages/browser-utils/src/browser/web-vitals/getINP.ts @@ -14,13 +14,14 @@ * limitations under the License. */ +import { WINDOW } from '../types'; import { bindReporter } from './lib/bindReporter'; import { initMetric } from './lib/initMetric'; import { observe } from './lib/observe'; import { onHidden } from './lib/onHidden'; import { getInteractionCount, initInteractionCountPolyfill } from './lib/polyfills/interactionCountPolyfill'; -import type { ReportCallback, ReportOpts } from './types'; -import type { INPMetric } from './types/inp'; +import { whenActivated } from './lib/whenActivated'; +import type { INPMetric, INPReportCallback, MetricRatingThresholds, ReportOpts } from './types'; interface Interaction { id: number; @@ -28,12 +29,19 @@ interface Interaction { entries: PerformanceEventTiming[]; } +/** Thresholds for INP. See https://web.dev/articles/inp#what_is_a_good_inp_score */ +export const INPThresholds: MetricRatingThresholds = [200, 500]; + +// Used to store the interaction count after a bfcache restore, since p98 +// interaction latencies should only consider the current navigation. +const prevInteractionCount = 0; + /** * Returns the interaction count since the last bfcache restore (or for the * full page lifecycle if there were no bfcache restores). */ -const getInteractionCountForNavigation = (): number => { - return getInteractionCount(); +const getInteractionCountForNavigation = () => { + return getInteractionCount() - prevInteractionCount; }; // To prevent unnecessary memory usage on pages with lots of interactions, @@ -54,7 +62,7 @@ const longestInteractionMap: { [interactionId: string]: Interaction } = {}; * entry is part of an existing interaction, it is merged and the latency * and entries list is updated as needed. */ -const processEntry = (entry: PerformanceEventTiming): void => { +const processEntry = (entry: PerformanceEventTiming) => { // The least-long of the 10 longest interactions. const minLongestInteraction = longestInteractionList[longestInteractionList.length - 1]; @@ -96,7 +104,7 @@ const processEntry = (entry: PerformanceEventTiming): void => { * Returns the estimated p98 longest interaction based on the stored * interaction candidates and the interaction count for the current page. */ -const estimateP98LongestInteraction = (): Interaction => { +const estimateP98LongestInteraction = () => { const candidateInteractionIndex = Math.min( longestInteractionList.length - 1, Math.floor(getInteractionCountForNavigation() / 50), @@ -106,7 +114,7 @@ const estimateP98LongestInteraction = (): Interaction => { }; /** - * Calculates the [INP](https://web.dev/responsiveness/) value for the current + * Calculates the [INP](https://web.dev/articles/inp) value for the current * page and calls the `callback` function once the value is ready, along with * the `event` performance entries reported for that interaction. The reported * value is a `DOMHighResTimeStamp`. @@ -116,7 +124,7 @@ const estimateP98LongestInteraction = (): Interaction => { * default threshold is `40`, which means INP scores of less than 40 are * reported as 0. Note that this will not affect your 75th percentile INP value * unless that value is also less than 40 (well below the recommended - * [good](https://web.dev/inp/#what-is-a-good-inp-score) threshold). + * [good](https://web.dev/articles/inp#what_is_a_good_inp_score) threshold). * * If the `reportAllChanges` configuration option is set to `true`, the * `callback` function will be called as soon as the value is initially @@ -132,84 +140,81 @@ const estimateP98LongestInteraction = (): Interaction => { * hidden. As a result, the `callback` function might be called multiple times * during the same page load._ */ -export const onINP = (onReport: ReportCallback, opts?: ReportOpts): void => { - // Set defaults - // eslint-disable-next-line no-param-reassign - opts = opts || {}; - - // https://web.dev/inp/#what's-a-%22good%22-inp-value - // const thresholds = [200, 500]; +export const onINP = (onReport: INPReportCallback, opts: ReportOpts = {}) => { + whenActivated(() => { + // TODO(philipwalton): remove once the polyfill is no longer needed. + initInteractionCountPolyfill(); + + const metric = initMetric('INP'); + // eslint-disable-next-line prefer-const + let report: ReturnType; + + const handleEntries = (entries: INPMetric['entries']) => { + entries.forEach(entry => { + if (entry.interactionId) { + processEntry(entry); + } - // TODO(philipwalton): remove once the polyfill is no longer needed. - initInteractionCountPolyfill(); + // Entries of type `first-input` don't currently have an `interactionId`, + // so to consider them in INP we have to first check that an existing + // entry doesn't match the `duration` and `startTime`. + // Note that this logic assumes that `event` entries are dispatched + // before `first-input` entries. This is true in Chrome (the only browser + // that currently supports INP). + // TODO(philipwalton): remove once crbug.com/1325826 is fixed. + if (entry.entryType === 'first-input') { + const noMatchingEntry = !longestInteractionList.some(interaction => { + return interaction.entries.some(prevEntry => { + return entry.duration === prevEntry.duration && entry.startTime === prevEntry.startTime; + }); + }); + if (noMatchingEntry) { + processEntry(entry); + } + } + }); - const metric = initMetric('INP'); - // eslint-disable-next-line prefer-const - let report: ReturnType; + const inp = estimateP98LongestInteraction(); - const handleEntries = (entries: INPMetric['entries']): void => { - entries.forEach(entry => { - if (entry.interactionId) { - processEntry(entry); + if (inp && inp.latency !== metric.value) { + metric.value = inp.latency; + metric.entries = inp.entries; + report(); } - - // Entries of type `first-input` don't currently have an `interactionId`, - // so to consider them in INP we have to first check that an existing - // entry doesn't match the `duration` and `startTime`. - // Note that this logic assumes that `event` entries are dispatched - // before `first-input` entries. This is true in Chrome but it is not - // true in Firefox; however, Firefox doesn't support interactionId, so - // it's not an issue at the moment. - // TODO(philipwalton): remove once crbug.com/1325826 is fixed. - if (entry.entryType === 'first-input') { - const noMatchingEntry = !longestInteractionList.some(interaction => { - return interaction.entries.some(prevEntry => { - return entry.duration === prevEntry.duration && entry.startTime === prevEntry.startTime; - }); - }); - if (noMatchingEntry) { - processEntry(entry); - } + }; + + const po = observe('event', handleEntries, { + // Event Timing entries have their durations rounded to the nearest 8ms, + // so a duration of 40ms would be any event that spans 2.5 or more frames + // at 60Hz. This threshold is chosen to strike a balance between usefulness + // and performance. Running this callback for any interaction that spans + // just one or two frames is likely not worth the insight that could be + // gained. + durationThreshold: opts.durationThreshold != null ? opts.durationThreshold : 40, + } as PerformanceObserverInit); + + report = bindReporter(onReport, metric, INPThresholds, opts.reportAllChanges); + + if (po) { + // If browser supports interactionId (and so supports INP), also + // observe entries of type `first-input`. This is useful in cases + // where the first interaction is less than the `durationThreshold`. + if ('PerformanceEventTiming' in WINDOW && 'interactionId' in PerformanceEventTiming.prototype) { + po.observe({ type: 'first-input', buffered: true }); } - }); - const inp = estimateP98LongestInteraction(); + onHidden(() => { + handleEntries(po.takeRecords() as INPMetric['entries']); - if (inp && inp.latency !== metric.value) { - metric.value = inp.latency; - metric.entries = inp.entries; - report(); - } - }; - - const po = observe('event', handleEntries, { - // Event Timing entries have their durations rounded to the nearest 8ms, - // so a duration of 40ms would be any event that spans 2.5 or more frames - // at 60Hz. This threshold is chosen to strike a balance between usefulness - // and performance. Running this callback for any interaction that spans - // just one or two frames is likely not worth the insight that could be - // gained. - durationThreshold: opts.durationThreshold || 40, - } as PerformanceObserverInit); - - report = bindReporter(onReport, metric, opts.reportAllChanges); - - if (po) { - // Also observe entries of type `first-input`. This is useful in cases - // where the first interaction is less than the `durationThreshold`. - po.observe({ type: 'first-input', buffered: true }); - - onHidden(() => { - handleEntries(po.takeRecords() as INPMetric['entries']); - - // If the interaction count shows that there were interactions but - // none were captured by the PerformanceObserver, report a latency of 0. - if (metric.value < 0 && getInteractionCountForNavigation() > 0) { - metric.value = 0; - metric.entries = []; - } + // If the interaction count shows that there were interactions but + // none were captured by the PerformanceObserver, report a latency of 0. + if (metric.value < 0 && getInteractionCountForNavigation() > 0) { + metric.value = 0; + metric.entries = []; + } - report(true); - }); - } + report(true); + }); + } + }); }; diff --git a/packages/browser-utils/src/browser/web-vitals/getLCP.ts b/packages/browser-utils/src/browser/web-vitals/getLCP.ts new file mode 100644 index 000000000000..facda8ce7a9d --- /dev/null +++ b/packages/browser-utils/src/browser/web-vitals/getLCP.ts @@ -0,0 +1,94 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { bindReporter } from './lib/bindReporter'; +import { getActivationStart } from './lib/getActivationStart'; +import { getVisibilityWatcher } from './lib/getVisibilityWatcher'; +import { initMetric } from './lib/initMetric'; +import { observe } from './lib/observe'; +import { onHidden } from './lib/onHidden'; +import { runOnce } from './lib/runOnce'; +import { whenActivated } from './lib/whenActivated'; +import type { LCPMetric, LCPReportCallback, MetricRatingThresholds, ReportOpts } from './types'; + +/** Thresholds for LCP. See https://web.dev/articles/lcp#what_is_a_good_lcp_score */ +export const LCPThresholds: MetricRatingThresholds = [2500, 4000]; + +const reportedMetricIDs: Record = {}; + +/** + * Calculates the [LCP](https://web.dev/articles/lcp) value for the current page and + * calls the `callback` function once the value is ready (along with the + * relevant `largest-contentful-paint` performance entry used to determine the + * value). The reported value is a `DOMHighResTimeStamp`. + * + * If the `reportAllChanges` configuration option is set to `true`, the + * `callback` function will be called any time a new `largest-contentful-paint` + * performance entry is dispatched, or once the final value of the metric has + * been determined. + */ +export const onLCP = (onReport: LCPReportCallback, opts: ReportOpts = {}) => { + whenActivated(() => { + const visibilityWatcher = getVisibilityWatcher(); + const metric = initMetric('LCP'); + let report: ReturnType; + + const handleEntries = (entries: LCPMetric['entries']) => { + const lastEntry = entries[entries.length - 1] as LargestContentfulPaint; + if (lastEntry) { + // Only report if the page wasn't hidden prior to LCP. + if (lastEntry.startTime < visibilityWatcher.firstHiddenTime) { + // The startTime attribute returns the value of the renderTime if it is + // not 0, and the value of the loadTime otherwise. The activationStart + // reference is used because LCP should be relative to page activation + // rather than navigation start if the page was prerendered. But in cases + // where `activationStart` occurs after the LCP, this time should be + // clamped at 0. + metric.value = Math.max(lastEntry.startTime - getActivationStart(), 0); + metric.entries = [lastEntry]; + report(); + } + } + }; + + const po = observe('largest-contentful-paint', handleEntries); + + if (po) { + report = bindReporter(onReport, metric, LCPThresholds, opts.reportAllChanges); + + const stopListening = runOnce(() => { + if (!reportedMetricIDs[metric.id]) { + handleEntries(po.takeRecords() as LCPMetric['entries']); + po.disconnect(); + reportedMetricIDs[metric.id] = true; + report(true); + } + }); + + // Stop listening after input. Note: while scrolling is an input that + // stops LCP observation, it's unreliable since it can be programmatically + // generated. See: https://github.com/GoogleChrome/web-vitals/issues/75 + ['keydown', 'click'].forEach(type => { + // Wrap in a setTimeout so the callback is run in a separate task + // to avoid extending the keyboard/click handler to reduce INP impact + // https://github.com/GoogleChrome/web-vitals/issues/383 + addEventListener(type, () => setTimeout(stopListening, 0), true); + }); + + onHidden(stopListening); + } + }); +}; diff --git a/packages/tracing-internal/src/browser/web-vitals/lib/bindReporter.ts b/packages/browser-utils/src/browser/web-vitals/lib/bindReporter.ts similarity index 67% rename from packages/tracing-internal/src/browser/web-vitals/lib/bindReporter.ts rename to packages/browser-utils/src/browser/web-vitals/lib/bindReporter.ts index 79f3f874e2d8..43fdc8d9e541 100644 --- a/packages/tracing-internal/src/browser/web-vitals/lib/bindReporter.ts +++ b/packages/browser-utils/src/browser/web-vitals/lib/bindReporter.ts @@ -14,13 +14,24 @@ * limitations under the License. */ -import type { Metric, ReportCallback } from '../types'; +import type { MetricRatingThresholds, MetricType } from '../types'; -export const bindReporter = ( - callback: ReportCallback, - metric: Metric, +const getRating = (value: number, thresholds: MetricRatingThresholds): MetricType['rating'] => { + if (value > thresholds[1]) { + return 'poor'; + } + if (value > thresholds[0]) { + return 'needs-improvement'; + } + return 'good'; +}; + +export const bindReporter = ( + callback: (metric: Extract) => void, + metric: Extract, + thresholds: MetricRatingThresholds, reportAllChanges?: boolean, -): ((forceReport?: boolean) => void) => { +) => { let prevValue: number; let delta: number; return (forceReport?: boolean) => { @@ -35,6 +46,7 @@ export const bindReporter = ( if (delta || prevValue === undefined) { prevValue = metric.value; metric.delta = delta; + metric.rating = getRating(metric.value, thresholds); callback(metric); } } diff --git a/packages/tracing-internal/src/browser/web-vitals/lib/generateUniqueID.ts b/packages/browser-utils/src/browser/web-vitals/lib/generateUniqueID.ts similarity index 94% rename from packages/tracing-internal/src/browser/web-vitals/lib/generateUniqueID.ts rename to packages/browser-utils/src/browser/web-vitals/lib/generateUniqueID.ts index a7972017d51c..bdecdc6220ad 100644 --- a/packages/tracing-internal/src/browser/web-vitals/lib/generateUniqueID.ts +++ b/packages/browser-utils/src/browser/web-vitals/lib/generateUniqueID.ts @@ -19,6 +19,6 @@ * number, the current timestamp with a 13-digit number integer. * @return {string} */ -export const generateUniqueID = (): string => { +export const generateUniqueID = () => { return `v3-${Date.now()}-${Math.floor(Math.random() * (9e12 - 1)) + 1e12}`; }; diff --git a/packages/tracing-internal/src/browser/web-vitals/lib/getActivationStart.ts b/packages/browser-utils/src/browser/web-vitals/lib/getActivationStart.ts similarity index 100% rename from packages/tracing-internal/src/browser/web-vitals/lib/getActivationStart.ts rename to packages/browser-utils/src/browser/web-vitals/lib/getActivationStart.ts diff --git a/packages/browser-utils/src/browser/web-vitals/lib/getNavigationEntry.ts b/packages/browser-utils/src/browser/web-vitals/lib/getNavigationEntry.ts new file mode 100644 index 000000000000..2fa455e3fbba --- /dev/null +++ b/packages/browser-utils/src/browser/web-vitals/lib/getNavigationEntry.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { WINDOW } from '../../types'; +import type { NavigationTimingPolyfillEntry } from '../types'; + +export const getNavigationEntry = (): PerformanceNavigationTiming | NavigationTimingPolyfillEntry | undefined => { + return WINDOW.performance && performance.getEntriesByType && performance.getEntriesByType('navigation')[0]; +}; diff --git a/packages/browser-utils/src/browser/web-vitals/lib/getVisibilityWatcher.ts b/packages/browser-utils/src/browser/web-vitals/lib/getVisibilityWatcher.ts new file mode 100644 index 000000000000..2cff287b2ae7 --- /dev/null +++ b/packages/browser-utils/src/browser/web-vitals/lib/getVisibilityWatcher.ts @@ -0,0 +1,76 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { WINDOW } from '../../types'; + +let firstHiddenTime = -1; + +const initHiddenTime = () => { + // If the document is hidden when this code runs, assume it was always + // hidden and the page was loaded in the background, with the one exception + // that visibility state is always 'hidden' during prerendering, so we have + // to ignore that case until prerendering finishes (see: `prerenderingchange` + // event logic below). + return WINDOW.document.visibilityState === 'hidden' && !WINDOW.document.prerendering ? 0 : Infinity; +}; + +const onVisibilityUpdate = (event: Event) => { + // If the document is 'hidden' and no previous hidden timestamp has been + // set, update it based on the current event data. + if (WINDOW.document.visibilityState === 'hidden' && firstHiddenTime > -1) { + // If the event is a 'visibilitychange' event, it means the page was + // visible prior to this change, so the event timestamp is the first + // hidden time. + // However, if the event is not a 'visibilitychange' event, then it must + // be a 'prerenderingchange' event, and the fact that the document is + // still 'hidden' from the above check means the tab was activated + // in a background state and so has always been hidden. + firstHiddenTime = event.type === 'visibilitychange' ? event.timeStamp : 0; + + // Remove all listeners now that a `firstHiddenTime` value has been set. + removeChangeListeners(); + } +}; + +const addChangeListeners = () => { + addEventListener('visibilitychange', onVisibilityUpdate, true); + // IMPORTANT: when a page is prerendering, its `visibilityState` is + // 'hidden', so in order to account for cases where this module checks for + // visibility during prerendering, an additional check after prerendering + // completes is also required. + addEventListener('prerenderingchange', onVisibilityUpdate, true); +}; + +const removeChangeListeners = () => { + removeEventListener('visibilitychange', onVisibilityUpdate, true); + removeEventListener('prerenderingchange', onVisibilityUpdate, true); +}; + +export const getVisibilityWatcher = () => { + if (firstHiddenTime < 0) { + // If the document is hidden when this code runs, assume it was hidden + // since navigation start. This isn't a perfect heuristic, but it's the + // best we can do until an API is available to support querying past + // visibilityState. + firstHiddenTime = initHiddenTime(); + addChangeListeners(); + } + return { + get firstHiddenTime() { + return firstHiddenTime; + }, + }; +}; diff --git a/packages/tracing-internal/src/browser/web-vitals/lib/initMetric.ts b/packages/browser-utils/src/browser/web-vitals/lib/initMetric.ts similarity index 65% rename from packages/tracing-internal/src/browser/web-vitals/lib/initMetric.ts rename to packages/browser-utils/src/browser/web-vitals/lib/initMetric.ts index 2fa5854fd6db..9098227ae1a4 100644 --- a/packages/tracing-internal/src/browser/web-vitals/lib/initMetric.ts +++ b/packages/browser-utils/src/browser/web-vitals/lib/initMetric.ts @@ -15,29 +15,34 @@ */ import { WINDOW } from '../../types'; -import type { Metric } from '../types'; +import type { MetricType } from '../types'; import { generateUniqueID } from './generateUniqueID'; import { getActivationStart } from './getActivationStart'; import { getNavigationEntry } from './getNavigationEntry'; -export const initMetric = (name: Metric['name'], value?: number): Metric => { +export const initMetric = (name: MetricName, value?: number) => { const navEntry = getNavigationEntry(); - let navigationType: Metric['navigationType'] = 'navigate'; + let navigationType: MetricType['navigationType'] = 'navigate'; if (navEntry) { if (WINDOW.document.prerendering || getActivationStart() > 0) { navigationType = 'prerender'; - } else { - navigationType = navEntry.type.replace(/_/g, '-') as Metric['navigationType']; + } else if (WINDOW.document.wasDiscarded) { + navigationType = 'restore'; + } else if (navEntry.type) { + navigationType = navEntry.type.replace(/_/g, '-') as MetricType['navigationType']; } } + // Use `entries` type specific for the metric. + const entries: Extract['entries'] = []; + return { name, value: typeof value === 'undefined' ? -1 : value, - rating: 'good', // Will be updated if the value changes. + rating: 'good' as const, // If needed, will be updated when reported. `const` to keep the type from widening to `string`. delta: 0, - entries: [], + entries, id: generateUniqueID(), navigationType, }; diff --git a/packages/tracing-internal/src/browser/web-vitals/lib/observe.ts b/packages/browser-utils/src/browser/web-vitals/lib/observe.ts similarity index 82% rename from packages/tracing-internal/src/browser/web-vitals/lib/observe.ts rename to packages/browser-utils/src/browser/web-vitals/lib/observe.ts index 685105d5c7dc..d763b14dfdf0 100644 --- a/packages/tracing-internal/src/browser/web-vitals/lib/observe.ts +++ b/packages/browser-utils/src/browser/web-vitals/lib/observe.ts @@ -14,11 +14,7 @@ * limitations under the License. */ -import type { FirstInputPolyfillEntry, NavigationTimingPolyfillEntry, PerformancePaintTiming } from '../types'; - -export interface PerformanceEntryHandler { - (entry: PerformanceEntry): void; -} +import type { FirstInputPolyfillEntry, NavigationTimingPolyfillEntry } from '../types'; interface PerformanceEntryMap { event: PerformanceEventTiming[]; @@ -47,7 +43,13 @@ export const observe = ( try { if (PerformanceObserver.supportedEntryTypes.includes(type)) { const po = new PerformanceObserver(list => { - callback(list.getEntries() as PerformanceEntryMap[K]); + // Delay by a microtask to workaround a bug in Safari where the + // callback is invoked immediately, rather than in a separate task. + // See: https://github.com/GoogleChrome/web-vitals/issues/277 + // eslint-disable-next-line @typescript-eslint/no-floating-promises + Promise.resolve().then(() => { + callback(list.getEntries() as PerformanceEntryMap[K]); + }); }); po.observe( Object.assign( diff --git a/packages/tracing-internal/src/browser/web-vitals/lib/onHidden.ts b/packages/browser-utils/src/browser/web-vitals/lib/onHidden.ts similarity index 78% rename from packages/tracing-internal/src/browser/web-vitals/lib/onHidden.ts rename to packages/browser-utils/src/browser/web-vitals/lib/onHidden.ts index 70152cadd16d..f9ec1dc94b90 100644 --- a/packages/tracing-internal/src/browser/web-vitals/lib/onHidden.ts +++ b/packages/browser-utils/src/browser/web-vitals/lib/onHidden.ts @@ -20,14 +20,10 @@ export interface OnHiddenCallback { (event: Event): void; } -export const onHidden = (cb: OnHiddenCallback, once?: boolean): void => { - const onHiddenOrPageHide = (event: Event): void => { +export const onHidden = (cb: OnHiddenCallback) => { + const onHiddenOrPageHide = (event: Event) => { if (event.type === 'pagehide' || WINDOW.document.visibilityState === 'hidden') { cb(event); - if (once) { - removeEventListener('visibilitychange', onHiddenOrPageHide, true); - removeEventListener('pagehide', onHiddenOrPageHide, true); - } } }; addEventListener('visibilitychange', onHiddenOrPageHide, true); diff --git a/packages/tracing-internal/src/browser/web-vitals/lib/polyfills/interactionCountPolyfill.ts b/packages/browser-utils/src/browser/web-vitals/lib/polyfills/interactionCountPolyfill.ts similarity index 100% rename from packages/tracing-internal/src/browser/web-vitals/lib/polyfills/interactionCountPolyfill.ts rename to packages/browser-utils/src/browser/web-vitals/lib/polyfills/interactionCountPolyfill.ts diff --git a/packages/browser-utils/src/browser/web-vitals/lib/runOnce.ts b/packages/browser-utils/src/browser/web-vitals/lib/runOnce.ts new file mode 100644 index 000000000000..c232fa16b487 --- /dev/null +++ b/packages/browser-utils/src/browser/web-vitals/lib/runOnce.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface RunOnceCallback { + (arg: unknown): void; +} + +export const runOnce = (cb: RunOnceCallback) => { + let called = false; + return (arg: unknown) => { + if (!called) { + cb(arg); + called = true; + } + }; +}; diff --git a/packages/browser-utils/src/browser/web-vitals/lib/whenActivated.ts b/packages/browser-utils/src/browser/web-vitals/lib/whenActivated.ts new file mode 100644 index 000000000000..a04af1dd0376 --- /dev/null +++ b/packages/browser-utils/src/browser/web-vitals/lib/whenActivated.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { WINDOW } from '../../types'; + +export const whenActivated = (callback: () => void) => { + if (WINDOW.document.prerendering) { + addEventListener('prerenderingchange', () => callback(), true); + } else { + callback(); + } +}; diff --git a/packages/browser-utils/src/browser/web-vitals/onFCP.ts b/packages/browser-utils/src/browser/web-vitals/onFCP.ts new file mode 100644 index 000000000000..b08973fefb40 --- /dev/null +++ b/packages/browser-utils/src/browser/web-vitals/onFCP.ts @@ -0,0 +1,65 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { bindReporter } from './lib/bindReporter'; +import { getActivationStart } from './lib/getActivationStart'; +import { getVisibilityWatcher } from './lib/getVisibilityWatcher'; +import { initMetric } from './lib/initMetric'; +import { observe } from './lib/observe'; +import { whenActivated } from './lib/whenActivated'; +import type { FCPMetric, FCPReportCallback, MetricRatingThresholds, ReportOpts } from './types'; + +/** Thresholds for FCP. See https://web.dev/articles/fcp#what_is_a_good_fcp_score */ +export const FCPThresholds: MetricRatingThresholds = [1800, 3000]; + +/** + * Calculates the [FCP](https://web.dev/articles/fcp) value for the current page and + * calls the `callback` function once the value is ready, along with the + * relevant `paint` performance entry used to determine the value. The reported + * value is a `DOMHighResTimeStamp`. + */ +export const onFCP = (onReport: FCPReportCallback, opts: ReportOpts = {}): void => { + whenActivated(() => { + const visibilityWatcher = getVisibilityWatcher(); + const metric = initMetric('FCP'); + let report: ReturnType; + + const handleEntries = (entries: FCPMetric['entries']) => { + (entries as PerformancePaintTiming[]).forEach(entry => { + if (entry.name === 'first-contentful-paint') { + po!.disconnect(); + + // Only report if the page wasn't hidden prior to the first paint. + if (entry.startTime < visibilityWatcher.firstHiddenTime) { + // The activationStart reference is used because FCP should be + // relative to page activation rather than navigation start if the + // page was prerendered. But in cases where `activationStart` occurs + // after the FCP, this time should be clamped at 0. + metric.value = Math.max(entry.startTime - getActivationStart(), 0); + metric.entries.push(entry); + report(true); + } + } + }); + }; + + const po = observe('paint', handleEntries); + + if (po) { + report = bindReporter(onReport, metric, FCPThresholds, opts!.reportAllChanges); + } + }); +}; diff --git a/packages/tracing-internal/src/browser/web-vitals/onTTFB.ts b/packages/browser-utils/src/browser/web-vitals/onTTFB.ts similarity index 69% rename from packages/tracing-internal/src/browser/web-vitals/onTTFB.ts rename to packages/browser-utils/src/browser/web-vitals/onTTFB.ts index 946141107fa8..13e6c6679309 100644 --- a/packages/tracing-internal/src/browser/web-vitals/onTTFB.ts +++ b/packages/browser-utils/src/browser/web-vitals/onTTFB.ts @@ -19,20 +19,19 @@ import { bindReporter } from './lib/bindReporter'; import { getActivationStart } from './lib/getActivationStart'; import { getNavigationEntry } from './lib/getNavigationEntry'; import { initMetric } from './lib/initMetric'; -import type { ReportCallback, ReportOpts } from './types'; -import type { TTFBMetric } from './types/ttfb'; +import { whenActivated } from './lib/whenActivated'; +import type { MetricRatingThresholds, ReportOpts, TTFBReportCallback } from './types'; + +/** Thresholds for TTFB. See https://web.dev/articles/ttfb#what_is_a_good_ttfb_score */ +export const TTFBThresholds: MetricRatingThresholds = [800, 1800]; /** * Runs in the next task after the page is done loading and/or prerendering. * @param callback */ -const whenReady = (callback: () => void): void => { - if (!WINDOW.document) { - return; - } - +const whenReady = (callback: () => void) => { if (WINDOW.document.prerendering) { - addEventListener('prerenderingchange', () => whenReady(callback), true); + whenActivated(() => whenReady(callback)); } else if (WINDOW.document.readyState !== 'complete') { addEventListener('load', () => whenReady(callback), true); } else { @@ -42,7 +41,7 @@ const whenReady = (callback: () => void): void => { }; /** - * Calculates the [TTFB](https://web.dev/time-to-first-byte/) value for the + * Calculates the [TTFB](https://web.dev/articles/ttfb) value for the * current page and calls the `callback` function once the page has loaded, * along with the relevant `navigation` performance entry used to determine the * value. The reported value is a `DOMHighResTimeStamp`. @@ -56,35 +55,31 @@ const whenReady = (callback: () => void): void => { * includes time spent on DNS lookup, connection negotiation, network latency, * and server processing time. */ -export const onTTFB = (onReport: ReportCallback, opts?: ReportOpts): void => { - // Set defaults - // eslint-disable-next-line no-param-reassign - opts = opts || {}; - - // https://web.dev/ttfb/#what-is-a-good-ttfb-score - // const thresholds = [800, 1800]; - +export const onTTFB = (onReport: TTFBReportCallback, opts: ReportOpts = {}) => { const metric = initMetric('TTFB'); - const report = bindReporter(onReport, metric, opts.reportAllChanges); + const report = bindReporter(onReport, metric, TTFBThresholds, opts.reportAllChanges); whenReady(() => { - const navEntry = getNavigationEntry() as TTFBMetric['entries'][number]; + const navEntry = getNavigationEntry(); if (navEntry) { + const responseStart = navEntry.responseStart; + + // In some cases no value is reported by the browser (for + // privacy/security reasons), and in other cases (bugs) the value is + // negative or is larger than the current page time. Ignore these cases: + // https://github.com/GoogleChrome/web-vitals/issues/137 + // https://github.com/GoogleChrome/web-vitals/issues/162 + // https://github.com/GoogleChrome/web-vitals/issues/275 + if (responseStart <= 0 || responseStart > performance.now()) return; + // The activationStart reference is used because TTFB should be // relative to page activation rather than navigation start if the // page was prerendered. But in cases where `activationStart` occurs // after the first byte is received, this time should be clamped at 0. - metric.value = Math.max(navEntry.responseStart - getActivationStart(), 0); - - // In some cases the value reported is negative or is larger - // than the current page time. Ignore these cases: - // https://github.com/GoogleChrome/web-vitals/issues/137 - // https://github.com/GoogleChrome/web-vitals/issues/162 - if (metric.value < 0 || metric.value > performance.now()) return; + metric.value = Math.max(responseStart - getActivationStart(), 0); metric.entries = [navEntry]; - report(true); } }); diff --git a/packages/tracing-internal/src/browser/web-vitals/types.ts b/packages/browser-utils/src/browser/web-vitals/types.ts similarity index 59% rename from packages/tracing-internal/src/browser/web-vitals/types.ts rename to packages/browser-utils/src/browser/web-vitals/types.ts index b4096b2678f6..41793221311b 100644 --- a/packages/tracing-internal/src/browser/web-vitals/types.ts +++ b/packages/browser-utils/src/browser/web-vitals/types.ts @@ -20,8 +20,11 @@ export * from './types/base'; export * from './types/polyfills'; export * from './types/cls'; +export * from './types/fcp'; export * from './types/fid'; +export * from './types/inp'; export * from './types/lcp'; +export * from './types/ttfb'; // -------------------------------------------------------------------------- // Web Vitals package globals @@ -36,21 +39,9 @@ export interface WebVitalsGlobal { declare global { interface Window { webVitals: WebVitalsGlobal; - - // Build flags: - __WEB_VITALS_POLYFILL__: boolean; } } -export type PerformancePaintTiming = PerformanceEntry; -export interface PerformanceEventTiming extends PerformanceEntry { - processingStart: DOMHighResTimeStamp; - processingEnd: DOMHighResTimeStamp; - duration: DOMHighResTimeStamp; - cancelable?: boolean; - target?: Element; -} - // -------------------------------------------------------------------------- // Everything below is modifications to built-in modules. // -------------------------------------------------------------------------- @@ -61,60 +52,13 @@ interface PerformanceEntryMap { paint: PerformancePaintTiming; } -export interface NavigatorNetworkInformation { - readonly connection?: NetworkInformation; -} - -// http://wicg.github.io/netinfo/#connection-types -type ConnectionType = 'bluetooth' | 'cellular' | 'ethernet' | 'mixed' | 'none' | 'other' | 'unknown' | 'wifi' | 'wimax'; - -// http://wicg.github.io/netinfo/#effectiveconnectiontype-enum -type EffectiveConnectionType = '2g' | '3g' | '4g' | 'slow-2g'; - -// http://wicg.github.io/netinfo/#dom-megabit -type Megabit = number; -// http://wicg.github.io/netinfo/#dom-millisecond -type Millisecond = number; - -// http://wicg.github.io/netinfo/#networkinformation-interface -interface NetworkInformation extends EventTarget { - // http://wicg.github.io/netinfo/#type-attribute - readonly type?: ConnectionType; - // http://wicg.github.io/netinfo/#effectivetype-attribute - readonly effectiveType?: EffectiveConnectionType; - // http://wicg.github.io/netinfo/#downlinkmax-attribute - readonly downlinkMax?: Megabit; - // http://wicg.github.io/netinfo/#downlink-attribute - readonly downlink?: Megabit; - // http://wicg.github.io/netinfo/#rtt-attribute - readonly rtt?: Millisecond; - // http://wicg.github.io/netinfo/#savedata-attribute - readonly saveData?: boolean; - // http://wicg.github.io/netinfo/#handling-changes-to-the-underlying-connection - onchange?: EventListener; -} - -// https://w3c.github.io/device-memory/#sec-device-memory-js-api -export interface NavigatorDeviceMemory { - readonly deviceMemory?: number; -} - -export type NavigationTimingPolyfillEntry = Omit< - PerformanceNavigationTiming, - | 'initiatorType' - | 'nextHopProtocol' - | 'redirectCount' - | 'transferSize' - | 'encodedBodySize' - | 'decodedBodySize' - | 'toJSON' ->; - // Update built-in types to be more accurate. declare global { - // https://wicg.github.io/nav-speculation/prerendering.html#document-prerendering interface Document { + // https://wicg.github.io/nav-speculation/prerendering.html#document-prerendering prerendering?: boolean; + // https://wicg.github.io/page-lifecycle/#sec-api + wasDiscarded?: boolean; } interface Performance { @@ -135,7 +79,6 @@ declare global { interface PerformanceEventTiming extends PerformanceEntry { duration: DOMHighResTimeStamp; interactionId?: number; - readonly target: Node | null; } // https://wicg.github.io/layout-instability/#sec-layout-shift-attribution diff --git a/packages/tracing-internal/src/browser/web-vitals/types/base.ts b/packages/browser-utils/src/browser/web-vitals/types/base.ts similarity index 71% rename from packages/tracing-internal/src/browser/web-vitals/types/base.ts rename to packages/browser-utils/src/browser/web-vitals/types/base.ts index 6d748d7843b4..6279edffabd4 100644 --- a/packages/tracing-internal/src/browser/web-vitals/types/base.ts +++ b/packages/browser-utils/src/browser/web-vitals/types/base.ts @@ -14,7 +14,13 @@ * limitations under the License. */ +import type { CLSMetric } from './cls'; +import type { FCPMetric } from './fcp'; +import type { FIDMetric } from './fid'; +import type { INPMetric } from './inp'; +import type { LCPMetric } from './lcp'; import type { FirstInputPolyfillEntry, NavigationTimingPolyfillEntry } from './polyfills'; +import type { TTFBMetric } from './ttfb'; export interface Metric { /** @@ -58,15 +64,22 @@ export interface Metric { entries: (PerformanceEntry | LayoutShift | FirstInputPolyfillEntry | NavigationTimingPolyfillEntry)[]; /** - * The type of navigation + * The type of navigation. * - * Navigation Timing API (or `undefined` if the browser doesn't - * support that API). For pages that are restored from the bfcache, this - * value will be 'back-forward-cache'. + * This will be the value returned by the Navigation Timing API (or + * `undefined` if the browser doesn't support that API), with the following + * exceptions: + * - 'back-forward-cache': for pages that are restored from the bfcache. + * - 'prerender': for pages that were prerendered. + * - 'restore': for pages that were discarded by the browser and then + * restored by the user. */ - navigationType: 'navigate' | 'reload' | 'back-forward' | 'back-forward-cache' | 'prerender'; + navigationType: 'navigate' | 'reload' | 'back-forward' | 'back-forward-cache' | 'prerender' | 'restore'; } +/** The union of supported metric types. */ +export type MetricType = CLSMetric | FCPMetric | FIDMetric | INPMetric | LCPMetric | TTFBMetric; + /** * A version of the `Metric` that is used with the attribution build. */ @@ -79,8 +92,23 @@ export interface MetricWithAttribution extends Metric { attribution: { [key: string]: unknown }; } +/** + * The thresholds of metric's "good", "needs improvement", and "poor" ratings. + * + * - Metric values up to and including [0] are rated "good" + * - Metric values up to and including [1] are rated "needs improvement" + * - Metric values above [1] are "poor" + * + * | Metric value | Rating | + * | --------------- | ------------------- | + * | ≦ [0] | "good" | + * | > [0] and ≦ [1] | "needs improvement" | + * | > [1] | "poor" | + */ +export type MetricRatingThresholds = [number, number]; + export interface ReportCallback { - (metric: Metric): void; + (metric: MetricType): void; } export interface ReportOpts { @@ -104,5 +132,3 @@ export interface ReportOpts { * loading. This is equivalent to the corresponding `readyState` value. */ export type LoadState = 'loading' | 'dom-interactive' | 'dom-content-loaded' | 'complete'; - -export type StopListening = undefined | void | (() => void); diff --git a/packages/tracing-internal/src/browser/web-vitals/types/cls.ts b/packages/browser-utils/src/browser/web-vitals/types/cls.ts similarity index 92% rename from packages/tracing-internal/src/browser/web-vitals/types/cls.ts rename to packages/browser-utils/src/browser/web-vitals/types/cls.ts index 0c97a5dde9aa..a95a8fb30770 100644 --- a/packages/tracing-internal/src/browser/web-vitals/types/cls.ts +++ b/packages/browser-utils/src/browser/web-vitals/types/cls.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { LoadState, Metric, ReportCallback } from './base'; +import type { LoadState, Metric } from './base'; /** * A CLS-specific version of the Metric object. @@ -76,13 +76,13 @@ export interface CLSMetricWithAttribution extends CLSMetric { /** * A CLS-specific version of the ReportCallback function. */ -export interface CLSReportCallback extends ReportCallback { +export interface CLSReportCallback { (metric: CLSMetric): void; } /** * A CLS-specific version of the ReportCallback function with attribution. */ -export interface CLSReportCallbackWithAttribution extends CLSReportCallback { +export interface CLSReportCallbackWithAttribution { (metric: CLSMetricWithAttribution): void; } diff --git a/packages/browser-utils/src/browser/web-vitals/types/fcp.ts b/packages/browser-utils/src/browser/web-vitals/types/fcp.ts new file mode 100644 index 000000000000..1a4c7d4962a3 --- /dev/null +++ b/packages/browser-utils/src/browser/web-vitals/types/fcp.ts @@ -0,0 +1,80 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { LoadState, Metric } from './base'; +import type { NavigationTimingPolyfillEntry } from './polyfills'; + +/** + * An FCP-specific version of the Metric object. + */ +export interface FCPMetric extends Metric { + name: 'FCP'; + entries: PerformancePaintTiming[]; +} + +/** + * An object containing potentially-helpful debugging information that + * can be sent along with the FCP value for the current page visit in order + * to help identify issues happening to real-users in the field. + */ +export interface FCPAttribution { + /** + * The time from when the user initiates loading the page until when the + * browser receives the first byte of the response (a.k.a. TTFB). + */ + timeToFirstByte: number; + /** + * The delta between TTFB and the first contentful paint (FCP). + */ + firstByteToFCP: number; + /** + * The loading state of the document at the time when FCP `occurred (see + * `LoadState` for details). Ideally, documents can paint before they finish + * loading (e.g. the `loading` or `dom-interactive` phases). + */ + loadState: LoadState; + /** + * The `PerformancePaintTiming` entry corresponding to FCP. + */ + fcpEntry?: PerformancePaintTiming; + /** + * The `navigation` entry of the current page, which is useful for diagnosing + * general page load issues. This can be used to access `serverTiming` for example: + * navigationEntry?.serverTiming + */ + navigationEntry?: PerformanceNavigationTiming | NavigationTimingPolyfillEntry; +} + +/** + * An FCP-specific version of the Metric object with attribution. + */ +export interface FCPMetricWithAttribution extends FCPMetric { + attribution: FCPAttribution; +} + +/** + * An FCP-specific version of the ReportCallback function. + */ +export interface FCPReportCallback { + (metric: FCPMetric): void; +} + +/** + * An FCP-specific version of the ReportCallback function with attribution. + */ +export interface FCPReportCallbackWithAttribution { + (metric: FCPMetricWithAttribution): void; +} diff --git a/packages/tracing-internal/src/browser/web-vitals/types/fid.ts b/packages/browser-utils/src/browser/web-vitals/types/fid.ts similarity index 92% rename from packages/tracing-internal/src/browser/web-vitals/types/fid.ts rename to packages/browser-utils/src/browser/web-vitals/types/fid.ts index 926f0675b90a..2001269c9b46 100644 --- a/packages/tracing-internal/src/browser/web-vitals/types/fid.ts +++ b/packages/browser-utils/src/browser/web-vitals/types/fid.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { LoadState, Metric, ReportCallback } from './base'; +import type { LoadState, Metric } from './base'; import type { FirstInputPolyfillEntry } from './polyfills'; /** @@ -69,13 +69,13 @@ export interface FIDMetricWithAttribution extends FIDMetric { /** * An FID-specific version of the ReportCallback function. */ -export interface FIDReportCallback extends ReportCallback { +export interface FIDReportCallback { (metric: FIDMetric): void; } /** * An FID-specific version of the ReportCallback function with attribution. */ -export interface FIDReportCallbackWithAttribution extends FIDReportCallback { +export interface FIDReportCallbackWithAttribution { (metric: FIDMetricWithAttribution): void; } diff --git a/packages/tracing-internal/src/browser/web-vitals/types/inp.ts b/packages/browser-utils/src/browser/web-vitals/types/inp.ts similarity index 91% rename from packages/tracing-internal/src/browser/web-vitals/types/inp.ts rename to packages/browser-utils/src/browser/web-vitals/types/inp.ts index 37e8333fb2d8..e83e0a0ece2a 100644 --- a/packages/tracing-internal/src/browser/web-vitals/types/inp.ts +++ b/packages/browser-utils/src/browser/web-vitals/types/inp.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { LoadState, Metric, ReportCallback } from './base'; +import type { LoadState, Metric } from './base'; /** * An INP-specific version of the Metric object. @@ -50,7 +50,7 @@ export interface INPAttribution { */ eventEntry?: PerformanceEventTiming; /** - * The loading state of the document at the time when the even corresponding + * The loading state of the document at the time when the event corresponding * to INP occurred (see `LoadState` for details). If the interaction occurred * while the document was loading and executing script (e.g. usually in the * `dom-interactive` phase) it can result in long delays. @@ -68,13 +68,13 @@ export interface INPMetricWithAttribution extends INPMetric { /** * An INP-specific version of the ReportCallback function. */ -export interface INPReportCallback extends ReportCallback { +export interface INPReportCallback { (metric: INPMetric): void; } /** * An INP-specific version of the ReportCallback function with attribution. */ -export interface INPReportCallbackWithAttribution extends INPReportCallback { +export interface INPReportCallbackWithAttribution { (metric: INPMetricWithAttribution): void; } diff --git a/packages/tracing-internal/src/browser/web-vitals/types/lcp.ts b/packages/browser-utils/src/browser/web-vitals/types/lcp.ts similarity index 84% rename from packages/tracing-internal/src/browser/web-vitals/types/lcp.ts rename to packages/browser-utils/src/browser/web-vitals/types/lcp.ts index c94573c1caaf..aaed1213508e 100644 --- a/packages/tracing-internal/src/browser/web-vitals/types/lcp.ts +++ b/packages/browser-utils/src/browser/web-vitals/types/lcp.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Metric, ReportCallback } from './base'; +import type { Metric } from './base'; import type { NavigationTimingPolyfillEntry } from './polyfills'; /** @@ -43,30 +43,31 @@ export interface LCPAttribution { /** * The time from when the user initiates loading the page until when the * browser receives the first byte of the response (a.k.a. TTFB). See - * [Optimize LCP](https://web.dev/optimize-lcp/) for details. + * [Optimize LCP](https://web.dev/articles/optimize-lcp) for details. */ timeToFirstByte: number; /** * The delta between TTFB and when the browser starts loading the LCP * resource (if there is one, otherwise 0). See [Optimize - * LCP](https://web.dev/optimize-lcp/) for details. + * LCP](https://web.dev/articles/optimize-lcp) for details. */ resourceLoadDelay: number; /** * The total time it takes to load the LCP resource itself (if there is one, - * otherwise 0). See [Optimize LCP](https://web.dev/optimize-lcp/) for + * otherwise 0). See [Optimize LCP](https://web.dev/articles/optimize-lcp) for * details. */ resourceLoadTime: number; /** * The delta between when the LCP resource finishes loading until the LCP * element is fully rendered. See [Optimize - * LCP](https://web.dev/optimize-lcp/) for details. + * LCP](https://web.dev/articles/optimize-lcp) for details. */ elementRenderDelay: number; /** * The `navigation` entry of the current page, which is useful for diagnosing - * general page load issues. + * general page load issues. This can be used to access `serverTiming` for example: + * navigationEntry?.serverTiming */ navigationEntry?: PerformanceNavigationTiming | NavigationTimingPolyfillEntry; /** @@ -90,13 +91,13 @@ export interface LCPMetricWithAttribution extends LCPMetric { /** * An LCP-specific version of the ReportCallback function. */ -export interface LCPReportCallback extends ReportCallback { +export interface LCPReportCallback { (metric: LCPMetric): void; } /** * An LCP-specific version of the ReportCallback function with attribution. */ -export interface LCPReportCallbackWithAttribution extends LCPReportCallback { +export interface LCPReportCallbackWithAttribution { (metric: LCPMetricWithAttribution): void; } diff --git a/packages/tracing-internal/src/browser/web-vitals/types/polyfills.ts b/packages/browser-utils/src/browser/web-vitals/types/polyfills.ts similarity index 100% rename from packages/tracing-internal/src/browser/web-vitals/types/polyfills.ts rename to packages/browser-utils/src/browser/web-vitals/types/polyfills.ts diff --git a/packages/tracing-internal/src/browser/web-vitals/types/ttfb.ts b/packages/browser-utils/src/browser/web-vitals/types/ttfb.ts similarity index 86% rename from packages/tracing-internal/src/browser/web-vitals/types/ttfb.ts rename to packages/browser-utils/src/browser/web-vitals/types/ttfb.ts index 86f1329ebee8..6a91394ad059 100644 --- a/packages/tracing-internal/src/browser/web-vitals/types/ttfb.ts +++ b/packages/browser-utils/src/browser/web-vitals/types/ttfb.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Metric, ReportCallback } from './base'; +import type { Metric } from './base'; import type { NavigationTimingPolyfillEntry } from './polyfills'; /** @@ -52,8 +52,9 @@ export interface TTFBAttribution { */ requestTime: number; /** - * The `PerformanceNavigationTiming` entry used to determine TTFB (or the - * polyfill entry in browsers that don't support Navigation Timing). + * The `navigation` entry of the current page, which is useful for diagnosing + * general page load issues. This can be used to access `serverTiming` for example: + * navigationEntry?.serverTiming */ navigationEntry?: PerformanceNavigationTiming | NavigationTimingPolyfillEntry; } @@ -68,13 +69,13 @@ export interface TTFBMetricWithAttribution extends TTFBMetric { /** * A TTFB-specific version of the ReportCallback function. */ -export interface TTFBReportCallback extends ReportCallback { +export interface TTFBReportCallback { (metric: TTFBMetric): void; } /** * A TTFB-specific version of the ReportCallback function with attribution. */ -export interface TTFBReportCallbackWithAttribution extends TTFBReportCallback { +export interface TTFBReportCallbackWithAttribution { (metric: TTFBMetricWithAttribution): void; } diff --git a/packages/node-experimental/src/debug-build.ts b/packages/browser-utils/src/common/debug-build.ts similarity index 100% rename from packages/node-experimental/src/debug-build.ts rename to packages/browser-utils/src/common/debug-build.ts diff --git a/packages/tracing-internal/src/index.ts b/packages/browser-utils/src/index.ts similarity index 66% rename from packages/tracing-internal/src/index.ts rename to packages/browser-utils/src/index.ts index 2dd4cf2f1768..019639e8c0b3 100644 --- a/packages/tracing-internal/src/index.ts +++ b/packages/browser-utils/src/index.ts @@ -1,17 +1,3 @@ -export * from './exports'; - -export { - Apollo, - Express, - GraphQL, - Mongo, - Mysql, - Postgres, - Prisma, - lazyLoadedNodePerformanceMonitoringIntegrations, -} from './node'; -export type { LazyLoadedIntegration } from './node'; - export { browserTracingIntegration, startBrowserTracingNavigationSpan, diff --git a/packages/tracing-internal/test/browser/backgroundtab.test.ts b/packages/browser-utils/test/browser/backgroundtab.test.ts similarity index 100% rename from packages/tracing-internal/test/browser/backgroundtab.test.ts rename to packages/browser-utils/test/browser/backgroundtab.test.ts diff --git a/packages/tracing-internal/test/browser/browserTracingIntegration.test.ts b/packages/browser-utils/test/browser/browserTracingIntegration.test.ts similarity index 94% rename from packages/tracing-internal/test/browser/browserTracingIntegration.test.ts rename to packages/browser-utils/test/browser/browserTracingIntegration.test.ts index 243ef14f159c..b537d684c8eb 100644 --- a/packages/tracing-internal/test/browser/browserTracingIntegration.test.ts +++ b/packages/browser-utils/test/browser/browserTracingIntegration.test.ts @@ -603,7 +603,7 @@ describe('browserTracingIntegration', () => { expect(spanToJSON(pageloadSpan!).data?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]).toBe('custom'); }); - it('sets the pageload span name on `scope.transactionName`', () => { + it('sets the navigation span name on `scope.transactionName`', () => { const client = new TestClient( getDefaultClientOptions({ integrations: [browserTracingIntegration()], @@ -612,10 +612,48 @@ describe('browserTracingIntegration', () => { setCurrentClient(client); client.init(); - startBrowserTracingPageLoadSpan(client, { name: 'test navigation span' }); + startBrowserTracingNavigationSpan(client, { name: 'test navigation span' }); expect(getCurrentScope().getScopeData().transactionName).toBe('test navigation span'); }); + + it("resets the scopes' propagationContexts", () => { + const client = new TestClient( + getDefaultClientOptions({ + integrations: [browserTracingIntegration()], + }), + ); + setCurrentClient(client); + client.init(); + + const oldIsolationScopePropCtx = getIsolationScope().getPropagationContext(); + const oldCurrentScopePropCtx = getCurrentScope().getPropagationContext(); + + startBrowserTracingNavigationSpan(client, { name: 'test navigation span' }); + + const newIsolationScopePropCtx = getIsolationScope().getPropagationContext(); + const newCurrentScopePropCtx = getCurrentScope().getPropagationContext(); + + expect(oldCurrentScopePropCtx).toEqual({ + spanId: expect.stringMatching(/[a-f0-9]{16}/), + traceId: expect.stringMatching(/[a-f0-9]{32}/), + }); + expect(oldIsolationScopePropCtx).toEqual({ + spanId: expect.stringMatching(/[a-f0-9]{16}/), + traceId: expect.stringMatching(/[a-f0-9]{32}/), + }); + expect(newCurrentScopePropCtx).toEqual({ + spanId: expect.stringMatching(/[a-f0-9]{16}/), + traceId: expect.stringMatching(/[a-f0-9]{32}/), + }); + expect(newIsolationScopePropCtx).toEqual({ + spanId: expect.stringMatching(/[a-f0-9]{16}/), + traceId: expect.stringMatching(/[a-f0-9]{32}/), + }); + + expect(newIsolationScopePropCtx?.traceId).not.toEqual(oldIsolationScopePropCtx?.traceId); + expect(newCurrentScopePropCtx?.traceId).not.toEqual(oldCurrentScopePropCtx?.traceId); + }); }); describe('using the tag data', () => { diff --git a/packages/tracing-internal/test/browser/metrics/index.test.ts b/packages/browser-utils/test/browser/metrics/index.test.ts similarity index 100% rename from packages/tracing-internal/test/browser/metrics/index.test.ts rename to packages/browser-utils/test/browser/metrics/index.test.ts diff --git a/packages/tracing-internal/test/browser/metrics/utils.test.ts b/packages/browser-utils/test/browser/metrics/utils.test.ts similarity index 100% rename from packages/tracing-internal/test/browser/metrics/utils.test.ts rename to packages/browser-utils/test/browser/metrics/utils.test.ts diff --git a/packages/tracing-internal/test/browser/request.test.ts b/packages/browser-utils/test/browser/request.test.ts similarity index 100% rename from packages/tracing-internal/test/browser/request.test.ts rename to packages/browser-utils/test/browser/request.test.ts diff --git a/packages/tracing-internal/test/utils/TestClient.ts b/packages/browser-utils/test/utils/TestClient.ts similarity index 100% rename from packages/tracing-internal/test/utils/TestClient.ts rename to packages/browser-utils/test/utils/TestClient.ts diff --git a/packages/tracing-internal/tsconfig.json b/packages/browser-utils/tsconfig.json similarity index 100% rename from packages/tracing-internal/tsconfig.json rename to packages/browser-utils/tsconfig.json diff --git a/packages/node-experimental/tsconfig.test.json b/packages/browser-utils/tsconfig.test.json similarity index 100% rename from packages/node-experimental/tsconfig.test.json rename to packages/browser-utils/tsconfig.test.json diff --git a/packages/tracing-internal/tsconfig.types.json b/packages/browser-utils/tsconfig.types.json similarity index 100% rename from packages/tracing-internal/tsconfig.types.json rename to packages/browser-utils/tsconfig.types.json diff --git a/packages/browser/package.json b/packages/browser/package.json index f099748dc96d..9cf3fba1d573 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -45,7 +45,7 @@ "@sentry-internal/feedback": "8.0.0-alpha.7", "@sentry-internal/replay": "8.0.0-alpha.7", "@sentry-internal/replay-canvas": "8.0.0-alpha.7", - "@sentry-internal/tracing": "8.0.0-alpha.7", + "@sentry-internal/browser-utils": "8.0.0-alpha.7", "@sentry/core": "8.0.0-alpha.7", "@sentry/types": "8.0.0-alpha.7", "@sentry/utils": "8.0.0-alpha.7" diff --git a/packages/browser/rollup.npm.config.mjs b/packages/browser/rollup.npm.config.mjs index 2edfdefdc4da..00251eea81fd 100644 --- a/packages/browser/rollup.npm.config.mjs +++ b/packages/browser/rollup.npm.config.mjs @@ -8,8 +8,11 @@ export default makeNPMConfigVariants( output: { // set exports to 'named' or 'auto' so that rollup doesn't warn exports: 'named', - // set preserveModules to false because we want to bundle everything into one file. - preserveModules: false, + // set preserveModules to true because we don't want to bundle everything into one file. + preserveModules: + process.env.SENTRY_BUILD_PRESERVE_MODULES === undefined + ? true + : Boolean(process.env.SENTRY_BUILD_PRESERVE_MODULES), }, }, }), diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index 8d581eaeda21..a83b1e549eba 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -10,7 +10,6 @@ export type { StackFrame, Stacktrace, Thread, - Transaction, User, Session, } from '@sentry/types'; diff --git a/packages/browser/src/index.bundle.base.ts b/packages/browser/src/index.bundle.base.ts index 5ccb0f1b1cf2..8570a426a7e7 100644 --- a/packages/browser/src/index.bundle.base.ts +++ b/packages/browser/src/index.bundle.base.ts @@ -1,21 +1 @@ -import type { IntegrationFn } from '@sentry/types/src'; - export * from './exports'; - -import type { Integration } from '@sentry/types'; - -import { WINDOW } from './helpers'; - -let windowIntegrations = {}; - -// This block is needed to add compatibility with the integrations packages when used with a CDN -if (WINDOW.Sentry && WINDOW.Sentry.Integrations) { - windowIntegrations = WINDOW.Sentry.Integrations; -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const INTEGRATIONS: Record Integration) | IntegrationFn> = { - ...windowIntegrations, -}; - -export { INTEGRATIONS as Integrations }; diff --git a/packages/browser/src/index.bundle.feedback.ts b/packages/browser/src/index.bundle.feedback.ts index 887cc2e864ac..de24e36e2a60 100644 --- a/packages/browser/src/index.bundle.feedback.ts +++ b/packages/browser/src/index.bundle.feedback.ts @@ -1,5 +1,5 @@ // This is exported so the loader does not fail when switching off Replay/Tracing -import { feedbackIntegration } from '@sentry-internal/feedback'; +import { feedbackIntegration, getFeedback } from '@sentry-internal/feedback'; import { addTracingExtensionsShim, browserTracingIntegrationShim, @@ -12,5 +12,6 @@ export { addTracingExtensionsShim as addTracingExtensions, replayIntegrationShim as replayIntegration, feedbackIntegration, + getFeedback, }; // Note: We do not export a shim for `Span` here, as that is quite complex and would blow up the bundle diff --git a/packages/browser/src/index.bundle.tracing.replay.feedback.ts b/packages/browser/src/index.bundle.tracing.replay.feedback.ts index 41db6a7cb5f7..6133fc2870f5 100644 --- a/packages/browser/src/index.bundle.tracing.replay.feedback.ts +++ b/packages/browser/src/index.bundle.tracing.replay.feedback.ts @@ -1,10 +1,10 @@ -import { feedbackIntegration } from '@sentry-internal/feedback'; -import { replayIntegration } from '@sentry-internal/replay'; import { browserTracingIntegration, startBrowserTracingNavigationSpan, startBrowserTracingPageLoadSpan, -} from '@sentry-internal/tracing'; +} from '@sentry-internal/browser-utils'; +import { feedbackIntegration, getFeedback } from '@sentry-internal/feedback'; +import { replayIntegration } from '@sentry-internal/replay'; import { addTracingExtensions } from '@sentry/core'; // We are patching the global object with our hub extension methods @@ -18,6 +18,7 @@ export { startSpanManual, withActiveSpan, getSpanDescendants, + setMeasurement, } from '@sentry/core'; export { @@ -27,6 +28,7 @@ export { addTracingExtensions, startBrowserTracingNavigationSpan, startBrowserTracingPageLoadSpan, + getFeedback, }; export * from './index.bundle.base'; diff --git a/packages/browser/src/index.bundle.tracing.replay.ts b/packages/browser/src/index.bundle.tracing.replay.ts index 7373fc8e5040..f949ea43541a 100644 --- a/packages/browser/src/index.bundle.tracing.replay.ts +++ b/packages/browser/src/index.bundle.tracing.replay.ts @@ -1,10 +1,10 @@ -import { feedbackIntegrationShim } from '@sentry-internal/integration-shims'; -import { replayIntegration } from '@sentry-internal/replay'; import { browserTracingIntegration, startBrowserTracingNavigationSpan, startBrowserTracingPageLoadSpan, -} from '@sentry-internal/tracing'; +} from '@sentry-internal/browser-utils'; +import { feedbackIntegrationShim } from '@sentry-internal/integration-shims'; +import { replayIntegration } from '@sentry-internal/replay'; import { addTracingExtensions } from '@sentry/core'; // We are patching the global object with our hub extension methods @@ -18,6 +18,7 @@ export { startSpanManual, withActiveSpan, getSpanDescendants, + setMeasurement, } from '@sentry/core'; export { diff --git a/packages/browser/src/index.bundle.tracing.ts b/packages/browser/src/index.bundle.tracing.ts index fcbc43974a91..1b4f89f935df 100644 --- a/packages/browser/src/index.bundle.tracing.ts +++ b/packages/browser/src/index.bundle.tracing.ts @@ -1,10 +1,10 @@ -// This is exported so the loader does not fail when switching off Replay -import { feedbackIntegrationShim, replayIntegrationShim } from '@sentry-internal/integration-shims'; import { browserTracingIntegration, startBrowserTracingNavigationSpan, startBrowserTracingPageLoadSpan, -} from '@sentry-internal/tracing'; +} from '@sentry-internal/browser-utils'; +// This is exported so the loader does not fail when switching off Replay +import { feedbackIntegrationShim, replayIntegrationShim } from '@sentry-internal/integration-shims'; import { addTracingExtensions } from '@sentry/core'; // We are patching the global object with our hub extension methods @@ -18,6 +18,7 @@ export { startSpanManual, withActiveSpan, getSpanDescendants, + setMeasurement, } from '@sentry/core'; export { diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 3108dc603d42..54dd27f8b8b5 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -1,22 +1,5 @@ export * from './exports'; -import { WINDOW } from './helpers'; - -let windowIntegrations = {}; - -// This block is needed to add compatibility with the integrations packages when used with a CDN -if (WINDOW.Sentry && WINDOW.Sentry.Integrations) { - windowIntegrations = WINDOW.Sentry.Integrations; -} - -/** @deprecated Import the integration function directly, e.g. `inboundFiltersIntegration()` instead of `new Integrations.InboundFilter(). */ -const INTEGRATIONS = { - ...windowIntegrations, -}; - -// eslint-disable-next-line deprecation/deprecation -export { INTEGRATIONS as Integrations }; - export { reportingObserverIntegration } from './integrations/reportingobserver'; export { httpClientIntegration } from './integrations/httpclient'; export { contextLinesIntegration } from './integrations/contextlines'; @@ -49,6 +32,7 @@ export { replayCanvasIntegration } from '@sentry-internal/replay-canvas'; export { feedbackIntegration, + getFeedback, sendFeedback, } from '@sentry-internal/feedback'; @@ -58,8 +42,8 @@ export { browserTracingIntegration, startBrowserTracingNavigationSpan, startBrowserTracingPageLoadSpan, -} from '@sentry-internal/tracing'; -export type { RequestInstrumentationOptions } from '@sentry-internal/tracing'; +} from '@sentry-internal/browser-utils'; +export type { RequestInstrumentationOptions } from '@sentry-internal/browser-utils'; export { addTracingExtensions, getActiveSpan, diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index d9e149ae4a0d..474432fedb48 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -1,4 +1,3 @@ -/* eslint-disable max-lines */ import { addBreadcrumb, defineIntegration, getClient } from '@sentry/core'; import type { Client, diff --git a/packages/browser/src/profiling/utils.ts b/packages/browser/src/profiling/utils.ts index 46ae0c07442a..a9dd735a3812 100644 --- a/packages/browser/src/profiling/utils.ts +++ b/packages/browser/src/profiling/utils.ts @@ -196,7 +196,7 @@ export function isProfiledTransactionEvent(event: Event): event is ProfiledEvent } /* - See packages/tracing-internal/src/browser/router.ts + See packages/browser-utils/src/browser/router.ts */ /** * diff --git a/packages/browser/src/sdk.ts b/packages/browser/src/sdk.ts index fc621ba7355a..88411c082106 100644 --- a/packages/browser/src/sdk.ts +++ b/packages/browser/src/sdk.ts @@ -32,6 +32,10 @@ import { makeFetchTransport } from './transports/fetch'; /** Get the default integrations for the browser SDK. */ export function getDefaultIntegrations(_options: Options): Integration[] { + /** + * Note: Please make sure this stays in sync with Angular SDK, which re-exports + * `getDefaultIntegrations` but with an adjusted set of integrations. + */ return [ inboundFiltersIntegration(), functionToStringIntegration(), diff --git a/packages/browser/test/integration/suites/api.js b/packages/browser/test/integration/suites/api.js deleted file mode 100644 index 462659e75ac7..000000000000 --- a/packages/browser/test/integration/suites/api.js +++ /dev/null @@ -1,162 +0,0 @@ -describe('API', function () { - it('should capture Sentry.captureMessage', function () { - return runInSandbox(sandbox, function () { - Sentry.captureMessage('Hello'); - }).then(function (summary) { - assert.equal(summary.events[0].message, 'Hello'); - }); - }); - - it('should capture Sentry.captureException', function () { - return runInSandbox(sandbox, function () { - try { - foo(); - } catch (e) { - Sentry.captureException(e); - } - }).then(function (summary) { - assert.isAtLeast(summary.events[0].exception.values[0].stacktrace.frames.length, 2); - assert.isAtMost(summary.events[0].exception.values[0].stacktrace.frames.length, 4); - }); - }); - - it('should generate a synthetic trace for captureException w/ non-errors', function () { - return runInSandbox(sandbox, function () { - throwNonError(); - }).then(function (summary) { - assert.isAtLeast(summary.events[0].exception.values[0].stacktrace.frames.length, 1); - assert.isAtMost(summary.events[0].exception.values[0].stacktrace.frames.length, 3); - }); - }); - - it('should have correct stacktrace order', function () { - return runInSandbox(sandbox, function () { - try { - foo(); - } catch (e) { - Sentry.captureException(e); - } - }).then(function (summary) { - assert.equal( - summary.events[0].exception.values[0].stacktrace.frames[ - summary.events[0].exception.values[0].stacktrace.frames.length - 1 - ].function, - 'bar', - ); - assert.isAtLeast(summary.events[0].exception.values[0].stacktrace.frames.length, 2); - assert.isAtMost(summary.events[0].exception.values[0].stacktrace.frames.length, 4); - }); - }); - - it('should have exception with type and value', function () { - return runInSandbox(sandbox, function () { - Sentry.captureException('this is my test exception'); - }).then(function (summary) { - assert.isNotEmpty(summary.events[0].exception.values[0].value); - assert.isNotEmpty(summary.events[0].exception.values[0].type); - }); - }); - - it('should reject duplicate, back-to-back errors from captureException', function () { - return runInSandbox(sandbox, function () { - // Different exceptions, don't dedupe - for (var i = 0; i < 2; i++) { - throwRandomError(); - } - - // Same exceptions and same stacktrace, dedupe - for (var j = 0; j < 2; j++) { - throwError(); - } - - // Same exceptions, different stacktrace (different line number), don't dedupe - throwSameConsecutiveErrors('bar'); - - // Same exception, with transaction in between, dedupe - throwError(); - Sentry.captureEvent({ - event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', - message: 'someMessage', - transaction: 'wat', - type: 'transaction', - }); - throwError(); - }).then(function (summary) { - // We have a length of one here since transactions don't go through beforeSend - // and we add events to summary in beforeSend - assert.equal(summary.events.length, 6); - assert.match(summary.events[0].exception.values[0].value, /Exception no \d+/); - assert.match(summary.events[1].exception.values[0].value, /Exception no \d+/); - assert.equal(summary.events[2].exception.values[0].value, 'foo'); - assert.equal(summary.events[3].exception.values[0].value, 'bar'); - assert.equal(summary.events[4].exception.values[0].value, 'bar'); - assert.equal(summary.events[5].exception.values[0].value, 'foo'); - }); - }); - - it('should not reject back-to-back errors with different stack traces', function () { - return runInSandbox(sandbox, function () { - // same error message, but different stacks means that these are considered - // different errors - - // stack: - // bar - try { - bar(); // declared in frame.html - } catch (e) { - Sentry.captureException(e); - } - - // stack (different # frames): - // bar - // foo - try { - foo(); // declared in frame.html - } catch (e) { - Sentry.captureException(e); - } - - // stack (same # frames, different frames): - // bar - // foo2 - try { - foo2(); // declared in frame.html - } catch (e) { - Sentry.captureException(e); - } - }).then(function (summary) { - // NOTE: regex because exact error message differs per-browser - assert.match(summary.events[0].exception.values[0].value, /baz/); - assert.equal(summary.events[0].exception.values[0].type, 'ReferenceError'); - assert.match(summary.events[1].exception.values[0].value, /baz/); - assert.equal(summary.events[1].exception.values[0].type, 'ReferenceError'); - assert.match(summary.events[2].exception.values[0].value, /baz/); - assert.equal(summary.events[2].exception.values[0].type, 'ReferenceError'); - }); - }); - - it('should reject duplicate, back-to-back messages from captureMessage', function () { - return runInSandbox(sandbox, function () { - // Different messages, don't dedupe - for (var i = 0; i < 2; i++) { - captureRandomMessage(); - } - - // Same messages and same stacktrace, dedupe - for (var j = 0; j < 2; j++) { - captureMessage('same message, same stacktrace'); - } - - // Same messages, different stacktrace (different line number), don't dedupe - captureSameConsecutiveMessages('same message, different stacktrace'); - }).then(function (summary) { - // On the async loader since we replay all messages from the same location, - // so we actually only receive 4 summary.events - assert.match(summary.events[0].message, /Message no \d+/); - assert.match(summary.events[1].message, /Message no \d+/); - assert.equal(summary.events[2].message, 'same message, same stacktrace'); - assert.equal(summary.events[3].message, 'same message, different stacktrace'); - !IS_LOADER && assert.equal(summary.events[4].message, 'same message, different stacktrace'); - }); - }); -}); diff --git a/packages/browser/test/integration/suites/breadcrumbs.js b/packages/browser/test/integration/suites/breadcrumbs.js deleted file mode 100644 index f0a0395bbe3f..000000000000 --- a/packages/browser/test/integration/suites/breadcrumbs.js +++ /dev/null @@ -1,786 +0,0 @@ -describe('breadcrumbs', function () { - it(optional('should record an XMLHttpRequest with a handler', IS_LOADER), function () { - return runInSandbox(sandbox, { manual: true }, function () { - var xhr = new XMLHttpRequest(); - xhr.open('GET', '/base/subjects/example.json'); - xhr.onreadystatechange = function () {}; - xhr.send(); - waitForXHR(xhr, function () { - Sentry.captureMessage('test'); - window.finalizeManualTest(); - }); - }).then(function (summary) { - // The async loader doesn't wrap XHR - if (IS_LOADER) { - return; - } - assert.equal(summary.breadcrumbs.length, 1); - assert.equal(summary.breadcrumbs[0].type, 'http'); - assert.equal(summary.breadcrumbs[0].category, 'xhr'); - assert.equal(summary.breadcrumbs[0].data.method, 'GET'); - }); - }); - - it(optional('should record an XMLHttpRequest with a handler attached after send was called', IS_LOADER), function () { - return runInSandbox(sandbox, { manual: true }, function () { - var xhr = new XMLHttpRequest(); - xhr.open('GET', '/base/subjects/example.json'); - xhr.send(); - xhr.onreadystatechange = function () { - window.handlerCalled = true; - }; - waitForXHR(xhr, function () { - Sentry.captureMessage('test'); - window.finalizeManualTest(); - }); - }).then(function (summary) { - // The async loader doesn't wrap XHR - if (IS_LOADER) { - return; - } - assert.equal(summary.breadcrumbs.length, 1); - assert.equal(summary.breadcrumbs[0].type, 'http'); - assert.equal(summary.breadcrumbs[0].category, 'xhr'); - assert.equal(summary.breadcrumbs[0].data.method, 'GET'); - assert.typeOf(summary.breadcrumbs[0].timestamp, 'number'); - assert.isTrue(summary.window.handlerCalled); - delete summary.window.handlerCalled; - }); - }); - - it(optional('should record an XMLHttpRequest without any handlers set', IS_LOADER), function () { - return runInSandbox(sandbox, { manual: true }, function () { - var xhr = new XMLHttpRequest(); - xhr.open('get', '/base/subjects/example.json'); - xhr.send(); - waitForXHR(xhr, function () { - Sentry.captureMessage('test'); - window.finalizeManualTest(); - }); - }).then(function (summary) { - // The async loader doesn't wrap XHR - if (IS_LOADER) { - return; - } - assert.equal(summary.breadcrumbs.length, 1); - assert.equal(summary.breadcrumbs[0].type, 'http'); - assert.equal(summary.breadcrumbs[0].category, 'xhr'); - assert.equal(summary.breadcrumbs[0].data.method, 'GET'); - assert.isUndefined(summary.breadcrumbs[0].data.input); - // To make sure that we are not providing this key for non-post requests - assert.equal(summary.breadcrumbHints[0].input, undefined); - }); - }); - - it(optional('should give access to request body for XMLHttpRequest POST requests', IS_LOADER), function () { - return runInSandbox(sandbox, { manual: true }, function () { - var xhr = new XMLHttpRequest(); - xhr.open('POST', '/base/subjects/example.json'); - xhr.send('{"foo":"bar"}'); - waitForXHR(xhr, function () { - Sentry.captureMessage('test'); - window.finalizeManualTest(); - }); - }).then(function (summary) { - // The async loader doesn't wrap XHR - if (IS_LOADER) { - return; - } - assert.equal(summary.breadcrumbs.length, 1); - assert.equal(summary.breadcrumbs[0].type, 'http'); - assert.equal(summary.breadcrumbs[0].category, 'xhr'); - assert.equal(summary.breadcrumbs[0].data.method, 'POST'); - assert.isUndefined(summary.breadcrumbs[0].data.input); - assert.equal(summary.breadcrumbHints[0].input, '{"foo":"bar"}'); - }); - }); - - it('should record a fetch request', function () { - return runInSandbox(sandbox, { manual: true }, function () { - fetch('/base/subjects/example.json', { - method: 'Get', - }) - .then( - function () { - Sentry.captureMessage('test'); - }, - function () { - Sentry.captureMessage('test'); - }, - ) - .then(function () { - window.finalizeManualTest(); - }) - .catch(function () { - window.finalizeManualTest(); - }); - }).then(function (summary) { - if (IS_LOADER) { - // The async loader doesn't wrap fetch, but we should receive the event without breadcrumbs - assert.lengthOf(summary.events, 1); - } else { - if (summary.window.supportsNativeFetch()) { - assert.equal(summary.breadcrumbs.length, 1); - assert.equal(summary.breadcrumbs[0].type, 'http'); - assert.equal(summary.breadcrumbs[0].category, 'fetch'); - assert.equal(summary.breadcrumbs[0].data.method, 'GET'); - assert.equal(summary.breadcrumbs[0].data.url, '/base/subjects/example.json'); - } else { - // otherwise we use a fetch polyfill based on xhr - assert.equal(summary.breadcrumbs.length, 1); - assert.equal(summary.breadcrumbs[0].type, 'http'); - assert.equal(summary.breadcrumbs[0].category, 'xhr'); - assert.equal(summary.breadcrumbs[0].data.method, 'GET'); - assert.equal(summary.breadcrumbs[0].data.url, '/base/subjects/example.json'); - } - } - }); - }); - - it('should record a fetch request with Request obj instead of URL string', function () { - return runInSandbox(sandbox, { manual: true }, function () { - fetch(new Request('/base/subjects/example.json')) - .then( - function () { - Sentry.captureMessage('test'); - }, - function () { - Sentry.captureMessage('test'); - }, - ) - .then(function () { - window.finalizeManualTest(); - }) - .catch(function () { - window.finalizeManualTest(); - }); - }).then(function (summary) { - if (IS_LOADER) { - // The async loader doesn't wrap fetch, but we should receive the event without breadcrumbs - assert.lengthOf(summary.events, 1); - } else { - if (summary.window.supportsNativeFetch()) { - assert.equal(summary.breadcrumbs.length, 1); - assert.equal(summary.breadcrumbs[0].type, 'http'); - assert.equal(summary.breadcrumbs[0].category, 'fetch'); - assert.equal(summary.breadcrumbs[0].data.method, 'GET'); - // Request constructor normalizes the url - assert.ok(summary.breadcrumbs[0].data.url.indexOf('/base/subjects/example.json') !== -1); - } else { - // otherwise we use a fetch polyfill based on xhr - assert.equal(summary.breadcrumbs.length, 1); - assert.equal(summary.breadcrumbs[0].type, 'http'); - assert.equal(summary.breadcrumbs[0].category, 'xhr'); - assert.equal(summary.breadcrumbs[0].data.method, 'GET'); - assert.ok(summary.breadcrumbs[0].data.url.indexOf('/base/subjects/example.json') !== -1); - } - } - }); - }); - - it('should record a fetch request with an arbitrary type argument', function () { - return runInSandbox(sandbox, { manual: true }, function () { - fetch(123) - .then( - function () { - Sentry.captureMessage('test'); - }, - function () { - Sentry.captureMessage('test'); - }, - ) - .then(function () { - window.finalizeManualTest(); - }) - .catch(function () { - window.finalizeManualTest(); - }); - }).then(function (summary) { - if (IS_LOADER) { - // The async loader doesn't wrap fetch, but we should receive the event without breadcrumbs - assert.lengthOf(summary.events, 1); - } else { - if (summary.window.supportsNativeFetch()) { - assert.equal(summary.breadcrumbs.length, 1); - assert.equal(summary.breadcrumbs[0].type, 'http'); - assert.equal(summary.breadcrumbs[0].category, 'fetch'); - assert.equal(summary.breadcrumbs[0].data.method, 'GET'); - assert.ok(summary.breadcrumbs[0].data.url.indexOf('123') !== -1); - } else { - // otherwise we use a fetch polyfill based on xhr - assert.equal(summary.breadcrumbs.length, 1); - assert.equal(summary.breadcrumbs[0].type, 'http'); - assert.equal(summary.breadcrumbs[0].category, 'xhr'); - assert.equal(summary.breadcrumbs[0].data.method, 'GET'); - assert.ok(summary.breadcrumbs[0].data.url.indexOf('123') !== -1); - } - } - }); - }); - - it('should provide a hint for dom events that includes event name and event itself', function () { - return runInSandbox(sandbox, function () { - var input = document.getElementsByTagName('input')[0]; - var clickHandler = function () {}; - input.addEventListener('click', clickHandler); - var click = new MouseEvent('click'); - input.dispatchEvent(click); - Sentry.captureMessage('test'); - }).then(function (summary) { - if (IS_LOADER) { - // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs - assert.lengthOf(summary.events, 1); - } else { - assert.equal(summary.breadcrumbHints.length, 1); - assert.equal(summary.breadcrumbHints[0].name, 'click'); - assert.equal(summary.breadcrumbHints[0].event.target.tagName, 'INPUT'); - } - }); - }); - - it('should not fail with click or keypress handler with no callback', function () { - return runInSandbox(sandbox, function () { - var input = document.getElementsByTagName('input')[0]; - input.addEventListener('click', undefined); - input.addEventListener('keypress', undefined); - - var click = new MouseEvent('click'); - input.dispatchEvent(click); - - var keypress = new KeyboardEvent('keypress'); - input.dispatchEvent(keypress); - - Sentry.captureMessage('test'); - }).then(function (summary) { - if (IS_LOADER) { - // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs - assert.lengthOf(summary.events, 1); - } else { - assert.equal(summary.breadcrumbs.length, 2); - - assert.equal(summary.breadcrumbs[0].category, 'ui.click'); - assert.equal(summary.breadcrumbs[0].message, 'body > form#foo-form > input[name="foo"]'); - - assert.equal(summary.breadcrumbs[1].category, 'ui.input'); - assert.equal(summary.breadcrumbs[1].message, 'body > form#foo-form > input[name="foo"]'); - } - }); - }); - - it('should not fail with custom event', function () { - return runInSandbox(sandbox, function () { - var input = document.getElementsByTagName('input')[0]; - input.addEventListener('build', function (evt) { - evt.stopPropagation(); - }); - - var customEvent = new CustomEvent('build', { detail: 1 }); - input.dispatchEvent(customEvent); - - Sentry.captureMessage('test'); - }).then(function (summary) { - if (IS_LOADER) { - // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs - assert.lengthOf(summary.events, 1); - } else { - assert.equal(summary.breadcrumbs.length, 0); - } - }); - }); - - it('should not fail with custom event and handler with no callback', function () { - return runInSandbox(sandbox, function () { - var input = document.getElementsByTagName('input')[0]; - input.addEventListener('build', undefined); - - var customEvent = new CustomEvent('build', { detail: 1 }); - input.dispatchEvent(customEvent); - - Sentry.captureMessage('test'); - }).then(function (summary) { - if (IS_LOADER) { - // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs - assert.lengthOf(summary.events, 1); - } else { - assert.equal(summary.breadcrumbs.length, 0); - } - }); - }); - - it('should record a mouse click on element WITH click handler present', function () { - return runInSandbox(sandbox, function () { - // add an event listener to the input. we want to make sure that - // our breadcrumbs still work even if the page has an event listener - // on an element that cancels event bubbling - var input = document.getElementsByTagName('input')[0]; - var clickHandler = function (evt) { - evt.stopPropagation(); // don't bubble - }; - input.addEventListener('click', clickHandler); - - // click - var click = new MouseEvent('click'); - input.dispatchEvent(click); - - Sentry.captureMessage('test'); - }).then(function (summary) { - if (IS_LOADER) { - // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs - assert.lengthOf(summary.events, 1); - } else { - assert.equal(summary.breadcrumbs.length, 1); - - assert.equal(summary.breadcrumbs[0].category, 'ui.click'); - assert.equal(summary.breadcrumbs[0].message, 'body > form#foo-form > input[name="foo"]'); - } - }); - }); - - it('should record a mouse click on element WITHOUT click handler present', function () { - return runInSandbox(sandbox, function () { - // click - var click = new MouseEvent('click'); - var input = document.getElementsByTagName('input')[0]; - input.dispatchEvent(click); - - Sentry.captureMessage('test'); - }).then(function (summary) { - if (IS_LOADER) { - // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs - assert.lengthOf(summary.events, 1); - } else { - assert.equal(summary.breadcrumbs.length, 1); - - assert.equal(summary.breadcrumbs[0].category, 'ui.click'); - assert.equal(summary.breadcrumbs[0].message, 'body > form#foo-form > input[name="foo"]'); - } - }); - }); - - it('should only record a SINGLE mouse click for a tree of elements with event listeners', function () { - return runInSandbox(sandbox, function () { - var clickHandler = function () {}; - - // mousemove event shouldnt clobber subsequent "breadcrumbed" events (see #724) - document.querySelector('.a').addEventListener('mousemove', clickHandler); - - document.querySelector('.a').addEventListener('click', clickHandler); - document.querySelector('.b').addEventListener('click', clickHandler); - document.querySelector('.c').addEventListener('click', clickHandler); - - // click - var click = new MouseEvent('click'); - var input = document.querySelector('.a'); // leaf node - input.dispatchEvent(click); - - Sentry.captureMessage('test'); - }).then(function (summary) { - if (IS_LOADER) { - // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs - assert.lengthOf(summary.events, 1); - } else { - assert.equal(summary.breadcrumbs.length, 1); - - assert.equal(summary.breadcrumbs[0].category, 'ui.click'); - assert.equal(summary.breadcrumbs[0].message, 'body > div.c > div.b > div.a'); - } - }); - }); - - it('should bail out if accessing the `target` property of an event throws an exception', function () { - // see: https://github.com/getsentry/sentry-javascript/issues/768 - return runInSandbox(sandbox, function () { - // click - var click = new MouseEvent('click'); - function kaboom() { - throw new Error('lol'); - } - Object.defineProperty(click, 'target', { get: kaboom }); - - var input = document.querySelector('.a'); // leaf node - - Sentry.captureMessage('test'); - input.dispatchEvent(click); - }).then(function (summary) { - if (IS_LOADER) { - // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs - assert.lengthOf(summary.events, 1); - } else { - assert.equal(summary.breadcrumbs.length, 1); - assert.equal(summary.breadcrumbs[0].category, 'ui.click'); - assert.equal(summary.breadcrumbs[0].message, ''); - } - }); - }); - - it('should record consecutive keypress events into a single "input" breadcrumb', function () { - return runInSandbox(sandbox, function () { - // keypress twice - var keypress1 = new KeyboardEvent('keypress'); - var keypress2 = new KeyboardEvent('keypress'); - - var input = document.getElementsByTagName('input')[0]; - input.dispatchEvent(keypress1); - input.dispatchEvent(keypress2); - - Sentry.captureMessage('test'); - }).then(function (summary) { - if (IS_LOADER) { - // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs - assert.lengthOf(summary.events, 1); - } else { - assert.equal(summary.breadcrumbs.length, 1); - - assert.equal(summary.breadcrumbs[0].category, 'ui.input'); - assert.equal(summary.breadcrumbs[0].message, 'body > form#foo-form > input[name="foo"]'); - } - }); - }); - - it('should correctly capture multiple consecutive breadcrumbs if they are of different type', function () { - return runInSandbox(sandbox, function () { - var input = document.getElementsByTagName('input')[0]; - - var clickHandler = function () {}; - input.addEventListener('click', clickHandler); - var keypressHandler = function () {}; - input.addEventListener('keypress', keypressHandler); - - input.dispatchEvent(new MouseEvent('click')); - input.dispatchEvent(new KeyboardEvent('keypress')); - - Sentry.captureMessage('test'); - }).then(function (summary) { - if (IS_LOADER) { - // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs - assert.lengthOf(summary.events, 1); - } else { - // Breadcrumb should be captured by the global event listeners, not a specific one - assert.equal(summary.breadcrumbs.length, 2); - assert.equal(summary.breadcrumbs[0].category, 'ui.click'); - assert.equal(summary.breadcrumbs[0].message, 'body > form#foo-form > input[name="foo"]'); - assert.equal(summary.breadcrumbs[1].category, 'ui.input'); - assert.equal(summary.breadcrumbs[0].message, 'body > form#foo-form > input[name="foo"]'); - assert.equal(summary.breadcrumbHints[0].global, false); - assert.equal(summary.breadcrumbHints[1].global, false); - } - }); - }); - - it('should debounce multiple consecutive identical breadcrumbs but allow for switching to a different type', function () { - return runInSandbox(sandbox, function () { - var input = document.getElementsByTagName('input')[0]; - - var clickHandler = function () {}; - input.addEventListener('click', clickHandler); - var keypressHandler = function () {}; - input.addEventListener('keypress', keypressHandler); - - input.dispatchEvent(new MouseEvent('click')); - input.dispatchEvent(new MouseEvent('click')); - input.dispatchEvent(new MouseEvent('click')); - input.dispatchEvent(new KeyboardEvent('keypress')); - input.dispatchEvent(new KeyboardEvent('keypress')); - input.dispatchEvent(new KeyboardEvent('keypress')); - - Sentry.captureMessage('test'); - }).then(function (summary) { - if (IS_LOADER) { - // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs - assert.lengthOf(summary.events, 1); - } else { - // Breadcrumb should be captured by the global event listeners, not a specific one - assert.equal(summary.breadcrumbs.length, 2); - assert.equal(summary.breadcrumbs[0].category, 'ui.click'); - assert.equal(summary.breadcrumbs[0].message, 'body > form#foo-form > input[name="foo"]'); - assert.equal(summary.breadcrumbs[1].category, 'ui.input'); - assert.equal(summary.breadcrumbs[0].message, 'body > form#foo-form > input[name="foo"]'); - assert.equal(summary.breadcrumbHints[0].global, false); - assert.equal(summary.breadcrumbHints[1].global, false); - } - }); - }); - - it('should debounce multiple consecutive identical breadcrumbs but allow for switching to a different target', function () { - return runInSandbox(sandbox, function () { - var input = document.querySelector('#foo-form input'); - var div = document.querySelector('#foo-form div'); - - var clickHandler = function () {}; - input.addEventListener('click', clickHandler); - div.addEventListener('click', clickHandler); - - input.dispatchEvent(new MouseEvent('click')); - div.dispatchEvent(new MouseEvent('click')); - - Sentry.captureMessage('test'); - }).then(function (summary) { - if (IS_LOADER) { - // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs - assert.lengthOf(summary.events, 1); - } else { - // Breadcrumb should be captured by the global event listeners, not a specific one - assert.equal(summary.breadcrumbs.length, 2); - assert.equal(summary.breadcrumbs[0].category, 'ui.click'); - assert.equal(summary.breadcrumbs[0].message, 'body > form#foo-form > input[name="foo"]'); - assert.equal(summary.breadcrumbs[1].category, 'ui.click'); - assert.equal(summary.breadcrumbs[1].message, 'body > form#foo-form > div.contenteditable'); - assert.equal(summary.breadcrumbHints[0].global, false); - assert.equal(summary.breadcrumbHints[1].global, false); - } - }); - }); - - it(optional('should flush keypress breadcrumbs when an error is thrown', IS_LOADER), function () { - return runInSandbox(sandbox, function () { - // keypress - var keypress = new KeyboardEvent('keypress'); - var input = document.getElementsByTagName('input')[0]; - input.dispatchEvent(keypress); - foo(); // throw exception - }).then(function (summary) { - if (IS_LOADER) { - return; - } - // TODO: don't really understand what's going on here - // Why do we not catch an error here - - assert.equal(summary.breadcrumbs.length, 1); - assert.equal(summary.breadcrumbs[0].category, 'ui.input'); - assert.equal(summary.breadcrumbs[0].message, 'body > form#foo-form > input[name="foo"]'); - }); - }); - - it('should flush keypress breadcrumb when input event occurs immediately after', function () { - return runInSandbox(sandbox, function () { - // 1st keypress - var keypress1 = new KeyboardEvent('keypress'); - // click - var click = new MouseEvent('click'); - // 2nd keypress - var keypress2 = new KeyboardEvent('keypress'); - - var input = document.getElementsByTagName('input')[0]; - input.dispatchEvent(keypress1); - input.dispatchEvent(click); - input.dispatchEvent(keypress2); - - Sentry.captureMessage('test'); - }).then(function (summary) { - if (IS_LOADER) { - // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs - assert.lengthOf(summary.events, 1); - } else { - assert.equal(summary.breadcrumbs.length, 3); - - assert.equal(summary.breadcrumbs[0].category, 'ui.input'); - assert.equal(summary.breadcrumbs[0].message, 'body > form#foo-form > input[name="foo"]'); - - assert.equal(summary.breadcrumbs[1].category, 'ui.click'); - assert.equal(summary.breadcrumbs[1].message, 'body > form#foo-form > input[name="foo"]'); - - assert.equal(summary.breadcrumbs[2].category, 'ui.input'); - assert.equal(summary.breadcrumbs[2].message, 'body > form#foo-form > input[name="foo"]'); - } - }); - }); - - it('should record consecutive keypress events in a contenteditable into a single "input" breadcrumb', function () { - return runInSandbox(sandbox, function () { - // keypress twice - var keypress1 = new KeyboardEvent('keypress'); - var keypress2 = new KeyboardEvent('keypress'); - - var div = document.querySelector('[contenteditable]'); - div.dispatchEvent(keypress1); - div.dispatchEvent(keypress2); - - Sentry.captureMessage('test'); - }).then(function (summary) { - if (IS_LOADER) { - // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs - assert.lengthOf(summary.events, 1); - } else { - assert.equal(summary.breadcrumbs.length, 1); - - assert.equal(summary.breadcrumbs[0].category, 'ui.input'); - assert.equal(summary.breadcrumbs[0].message, 'body > form#foo-form > div.contenteditable'); - } - }); - }); - - it('should record click events that were handled using an object with handleEvent property and call original callback', function () { - return runInSandbox(sandbox, function () { - window.handleEventCalled = false; - - var input = document.getElementsByTagName('input')[0]; - input.addEventListener('click', { - handleEvent: function () { - window.handleEventCalled = true; - }, - }); - input.dispatchEvent(new MouseEvent('click')); - - Sentry.captureMessage('test'); - }).then(function (summary) { - if (IS_LOADER) { - // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs - assert.lengthOf(summary.events, 1); - } else { - assert.equal(summary.breadcrumbs.length, 1); - assert.equal(summary.breadcrumbs[0].category, 'ui.click'); - assert.equal(summary.breadcrumbs[0].message, 'body > form#foo-form > input[name="foo"]'); - - assert.equal(summary.window.handleEventCalled, true); - } - }); - }); - - it('should record keypress events that were handled using an object with handleEvent property and call original callback', function () { - return runInSandbox(sandbox, function () { - window.handleEventCalled = false; - - var input = document.getElementsByTagName('input')[0]; - input.addEventListener('keypress', { - handleEvent: function () { - window.handleEventCalled = true; - }, - }); - input.dispatchEvent(new KeyboardEvent('keypress')); - - Sentry.captureMessage('test'); - }).then(function (summary) { - if (IS_LOADER) { - // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs - assert.lengthOf(summary.events, 1); - } else { - assert.equal(summary.breadcrumbs.length, 1); - assert.equal(summary.breadcrumbs[0].category, 'ui.input'); - assert.equal(summary.breadcrumbs[0].message, 'body > form#foo-form > input[name="foo"]'); - - assert.equal(summary.window.handleEventCalled, true); - } - }); - }); - - it('should remove breadcrumb instrumentation when all event listeners are detached', function () { - return runInSandbox(sandbox, function () { - var input = document.getElementsByTagName('input')[0]; - - var clickHandler = function () {}; - var otherClickHandler = function () {}; - input.addEventListener('click', clickHandler); - input.addEventListener('click', otherClickHandler); - input.removeEventListener('click', clickHandler); - input.removeEventListener('click', otherClickHandler); - - var keypressHandler = function () {}; - var otherKeypressHandler = function () {}; - input.addEventListener('keypress', keypressHandler); - input.addEventListener('keypress', otherKeypressHandler); - input.removeEventListener('keypress', keypressHandler); - input.removeEventListener('keypress', otherKeypressHandler); - - input.dispatchEvent(new MouseEvent('click')); - input.dispatchEvent(new KeyboardEvent('keypress')); - - Sentry.captureMessage('test'); - }).then(function (summary) { - if (IS_LOADER) { - // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs - assert.lengthOf(summary.events, 1); - } else { - // Breadcrumb should be captured by the global event listeners, not a specific one - assert.equal(summary.breadcrumbs.length, 2); - assert.equal(summary.breadcrumbHints[0].global, true); - assert.equal(summary.breadcrumbHints[1].global, true); - } - }); - }); - - it( - optional('should record history.[pushState|replaceState] changes as navigation breadcrumbs', IS_LOADER), - function () { - return runInSandbox(sandbox, function () { - history.pushState({}, '', '/foo'); - history.pushState({}, '', '/bar?a=1#fragment'); - history.pushState({}, '', {}); // pushState calls toString on non-string args - history.pushState({}, '', null); // does nothing / no-op - // can't call history.back() because it will change url of parent document - // (e.g. document running mocha) ... instead just "emulate" a back button - // press by calling replaceState - history.replaceState({}, '', '/bar?a=1#fragment'); - Sentry.captureMessage('test'); - }).then(function (summary) { - if (IS_LOADER) { - // The async loader doesn't wrap history - return; - } - assert.equal(summary.breadcrumbs.length, 4); - assert.equal(summary.breadcrumbs[0].category, 'navigation'); // (start) => foo - assert.equal(summary.breadcrumbs[1].category, 'navigation'); // foo => bar?a=1#fragment - assert.equal(summary.breadcrumbs[2].category, 'navigation'); // bar?a=1#fragment => [object%20Object] - assert.equal(summary.breadcrumbs[3].category, 'navigation'); // [object%20Object] => bar?a=1#fragment (back button) - - assert.ok(/\/base\/variants\/.*\.html$/.test(summary.breadcrumbs[0].data.from), "'from' url is incorrect"); - assert.ok(/\/foo$/.test(summary.breadcrumbs[0].data.to), "'to' url is incorrect"); - - assert.ok(/\/foo$/.test(summary.breadcrumbs[1].data.from), "'from' url is incorrect"); - assert.ok(/\/bar\?a=1#fragment$/.test(summary.breadcrumbs[1].data.to), "'to' url is incorrect"); - - assert.ok(/\/bar\?a=1#fragment$/.test(summary.breadcrumbs[2].data.from), "'from' url is incorrect"); - assert.ok(/\[object Object\]$/.test(summary.breadcrumbs[2].data.to), "'to' url is incorrect"); - - assert.ok(/\[object Object\]$/.test(summary.breadcrumbs[3].data.from), "'from' url is incorrect"); - assert.ok(/\/bar\?a=1#fragment/.test(summary.breadcrumbs[3].data.to), "'to' url is incorrect"); - }); - }, - ); - - it(optional('should preserve native code detection compatibility', IS_LOADER), function () { - return runInSandbox(sandbox, { manual: true }, function () { - window.resolveTest(); - }).then(function () { - if (IS_LOADER) { - // The async loader doesn't wrap anything - return; - } - assert.include(Function.prototype.toString.call(window.setTimeout), '[native code]'); - assert.include(Function.prototype.toString.call(window.setInterval), '[native code]'); - assert.include(Function.prototype.toString.call(window.addEventListener), '[native code]'); - assert.include(Function.prototype.toString.call(window.removeEventListener), '[native code]'); - assert.include(Function.prototype.toString.call(window.requestAnimationFrame), '[native code]'); - if ('fetch' in window) { - assert.include(Function.prototype.toString.call(window.fetch), '[native code]'); - } - }); - }); - - it('should capture console breadcrumbs', function () { - return runInSandbox(sandbox, { manual: true }, function () { - window.allowConsoleBreadcrumbs = true; - var logs = document.createElement('script'); - logs.src = '/base/subjects/console-logs.js'; - logs.onload = function () { - window.finalizeManualTest(); - }; - document.head.appendChild(logs); - }).then(function (summary) { - if (IS_LOADER) { - // The async loader doesn't capture breadcrumbs, but we should receive the event without them - assert.lengthOf(summary.events, 1); - } else { - if ('assert' in console) { - assert.lengthOf(summary.breadcrumbs, 4); - assert.deepEqual(summary.breadcrumbs[3].data.arguments, ['math broke']); - } else { - assert.lengthOf(summary.breadcrumbs, 3); - } - - assert.deepEqual(summary.breadcrumbs[0].data.arguments, ['One']); - assert.deepEqual(summary.breadcrumbs[1].data.arguments, ['Two', { a: 1 }]); - assert.deepEqual(summary.breadcrumbs[2].data.arguments, ['Error 2', { b: { c: [] } }]); - } - }); - }); -}); diff --git a/packages/browser/test/integration/suites/shell.js b/packages/browser/test/integration/suites/shell.js index d3d89ed4b798..e1555623b495 100644 --- a/packages/browser/test/integration/suites/shell.js +++ b/packages/browser/test/integration/suites/shell.js @@ -23,11 +23,9 @@ function runVariant(variant) { * The test runner will replace each of these placeholders with the contents of the corresponding file. */ {{ suites/config.js }} // biome-ignore format: No trailing commas - {{ suites/api.js }} // biome-ignore format: No trailing commas {{ suites/onerror.js }} // biome-ignore format: No trailing commas {{ suites/onunhandledrejection.js }} // biome-ignore format: No trailing commas {{ suites/builtins.js }} // biome-ignore format: No trailing commas - {{ suites/breadcrumbs.js }} // biome-ignore format: No trailing commas {{ suites/loader.js }} // biome-ignore format: No trailing commas }); } diff --git a/packages/browser/test/unit/index.bundle.feedback.test.ts b/packages/browser/test/unit/index.bundle.feedback.test.ts index 4ccb6a9dc458..5a3451cb3ef0 100644 --- a/packages/browser/test/unit/index.bundle.feedback.test.ts +++ b/packages/browser/test/unit/index.bundle.feedback.test.ts @@ -5,10 +5,6 @@ import * as TracingReplayBundle from '../../src/index.bundle.feedback'; describe('index.bundle.feedback', () => { it('has correct exports', () => { - Object.keys(TracingReplayBundle.Integrations).forEach(key => { - expect((TracingReplayBundle.Integrations[key] as any).id).toStrictEqual(expect.any(String)); - }); - expect(TracingReplayBundle.replayIntegration).toBe(replayIntegrationShim); expect(TracingReplayBundle.feedbackIntegration).toBe(feedbackIntegration); }); diff --git a/packages/browser/test/unit/index.bundle.replay.test.ts b/packages/browser/test/unit/index.bundle.replay.test.ts index 56356e262eea..479e6b23393b 100644 --- a/packages/browser/test/unit/index.bundle.replay.test.ts +++ b/packages/browser/test/unit/index.bundle.replay.test.ts @@ -5,10 +5,6 @@ import * as TracingReplayBundle from '../../src/index.bundle.replay'; describe('index.bundle.replay', () => { it('has correct exports', () => { - Object.keys(TracingReplayBundle.Integrations).forEach(key => { - expect((TracingReplayBundle.Integrations[key] as any).id).toStrictEqual(expect.any(String)); - }); - expect(TracingReplayBundle.replayIntegration).toBe(replayIntegration); expect(TracingReplayBundle.feedbackIntegration).toBe(feedbackIntegrationShim); }); diff --git a/packages/browser/test/unit/index.bundle.test.ts b/packages/browser/test/unit/index.bundle.test.ts index 91cc4dea5229..9ef9f38d8db5 100644 --- a/packages/browser/test/unit/index.bundle.test.ts +++ b/packages/browser/test/unit/index.bundle.test.ts @@ -4,10 +4,6 @@ import * as TracingBundle from '../../src/index.bundle'; describe('index.bundle', () => { it('has correct exports', () => { - Object.keys(TracingBundle.Integrations).forEach(key => { - expect((TracingBundle.Integrations[key] as any).name).toStrictEqual(expect.any(String)); - }); - expect(TracingBundle.replayIntegration).toBe(replayIntegrationShim); expect(TracingBundle.feedbackIntegration).toBe(feedbackIntegrationShim); }); diff --git a/packages/browser/test/unit/index.bundle.tracing.replay.feedback.test.ts b/packages/browser/test/unit/index.bundle.tracing.replay.feedback.test.ts index 49c23d9685bb..a8440d160e2b 100644 --- a/packages/browser/test/unit/index.bundle.tracing.replay.feedback.test.ts +++ b/packages/browser/test/unit/index.bundle.tracing.replay.feedback.test.ts @@ -1,14 +1,10 @@ -import { browserTracingIntegration } from '@sentry-internal/tracing'; +import { browserTracingIntegration } from '@sentry-internal/browser-utils'; import { feedbackIntegration, replayIntegration } from '@sentry/browser'; import * as TracingReplayFeedbackBundle from '../../src/index.bundle.tracing.replay.feedback'; describe('index.bundle.tracing.replay.feedback', () => { it('has correct exports', () => { - Object.keys(TracingReplayFeedbackBundle.Integrations).forEach(key => { - expect((TracingReplayFeedbackBundle.Integrations[key] as any).id).toStrictEqual(expect.any(String)); - }); - expect(TracingReplayFeedbackBundle.replayIntegration).toBe(replayIntegration); expect(TracingReplayFeedbackBundle.browserTracingIntegration).toBe(browserTracingIntegration); expect(TracingReplayFeedbackBundle.feedbackIntegration).toBe(feedbackIntegration); diff --git a/packages/browser/test/unit/index.bundle.tracing.replay.test.ts b/packages/browser/test/unit/index.bundle.tracing.replay.test.ts index bdbb744f7873..18c286edffc9 100644 --- a/packages/browser/test/unit/index.bundle.tracing.replay.test.ts +++ b/packages/browser/test/unit/index.bundle.tracing.replay.test.ts @@ -1,15 +1,11 @@ +import { browserTracingIntegration } from '@sentry-internal/browser-utils'; import { feedbackIntegrationShim } from '@sentry-internal/integration-shims'; -import { browserTracingIntegration } from '@sentry-internal/tracing'; import { replayIntegration } from '@sentry/browser'; import * as TracingReplayBundle from '../../src/index.bundle.tracing.replay'; describe('index.bundle.tracing.replay', () => { it('has correct exports', () => { - Object.keys(TracingReplayBundle.Integrations).forEach(key => { - expect((TracingReplayBundle.Integrations[key] as any).id).toStrictEqual(expect.any(String)); - }); - expect(TracingReplayBundle.replayIntegration).toBe(replayIntegration); expect(TracingReplayBundle.browserTracingIntegration).toBe(browserTracingIntegration); diff --git a/packages/browser/test/unit/index.bundle.tracing.test.ts b/packages/browser/test/unit/index.bundle.tracing.test.ts index 4c8c37008fc4..1bb1ca19eec1 100644 --- a/packages/browser/test/unit/index.bundle.tracing.test.ts +++ b/packages/browser/test/unit/index.bundle.tracing.test.ts @@ -1,14 +1,10 @@ +import { browserTracingIntegration } from '@sentry-internal/browser-utils'; import { feedbackIntegrationShim, replayIntegrationShim } from '@sentry-internal/integration-shims'; -import { browserTracingIntegration } from '@sentry-internal/tracing'; import * as TracingBundle from '../../src/index.bundle.tracing'; describe('index.bundle.tracing', () => { it('has correct exports', () => { - Object.keys(TracingBundle.Integrations).forEach(key => { - expect((TracingBundle.Integrations[key] as any).id).toStrictEqual(expect.any(String)); - }); - expect(TracingBundle.replayIntegration).toBe(replayIntegrationShim); expect(TracingBundle.browserTracingIntegration).toBe(browserTracingIntegration); expect(TracingBundle.feedbackIntegration).toBe(feedbackIntegrationShim); diff --git a/packages/browser/test/unit/index.test.ts b/packages/browser/test/unit/index.test.ts index e97046c3eb65..2aae1410d438 100644 --- a/packages/browser/test/unit/index.test.ts +++ b/packages/browser/test/unit/index.test.ts @@ -7,7 +7,6 @@ import { } from '@sentry/core'; import * as utils from '@sentry/utils'; -import type { Event } from '../../src'; import { setCurrentClient } from '../../src'; import { BrowserClient, @@ -213,7 +212,7 @@ describe('SentryBrowser', () => { it('should capture a message', done => { const options = getDefaultBrowserClientOptions({ - beforeSend: (event: Event): Event | null => { + beforeSend: event => { expect(event.message).toBe('test'); expect(event.exception).toBeUndefined(); done(); @@ -227,7 +226,7 @@ describe('SentryBrowser', () => { it('should capture an event', done => { const options = getDefaultBrowserClientOptions({ - beforeSend: (event: Event): Event | null => { + beforeSend: event => { expect(event.message).toBe('event'); expect(event.exception).toBeUndefined(); done(); @@ -241,7 +240,7 @@ describe('SentryBrowser', () => { it('should set `platform` on events', done => { const options = getDefaultBrowserClientOptions({ - beforeSend: (event: Event): Event | null => { + beforeSend: event => { expect(event.platform).toBe('javascript'); done(); return event; diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index ae09717c34b4..db54dcdd6fb5 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -13,7 +13,6 @@ export type { StackFrame, Stacktrace, Thread, - Transaction, User, } from '@sentry/types'; export type { AddRequestDataToEventOptions } from '@sentry/utils'; @@ -98,12 +97,15 @@ export { setupExpressErrorHandler, fastifyIntegration, setupFastifyErrorHandler, + koaIntegration, + setupKoaErrorHandler, graphqlIntegration, mongoIntegration, mongooseIntegration, mysqlIntegration, mysql2Integration, nestIntegration, + setupNestErrorHandler, postgresIntegration, prismaIntegration, hapiIntegration, @@ -111,6 +113,7 @@ export { spotlightIntegration, initOpenTelemetry, spanToJSON, + trpcMiddleware, } from '@sentry/node'; export { @@ -125,9 +128,6 @@ export { export type { BunOptions } from './types'; export { BunClient } from './client'; -export { - getDefaultIntegrations, - init, -} from './sdk'; +export { getDefaultIntegrations, init } from './sdk'; export { bunServerIntegration } from './integrations/bunserver'; export { makeFetchTransport } from './transports'; diff --git a/packages/bun/src/integrations/bunserver.ts b/packages/bun/src/integrations/bunserver.ts index a530fc0517c2..93a3f94dd4a0 100644 --- a/packages/bun/src/integrations/bunserver.ts +++ b/packages/bun/src/integrations/bunserver.ts @@ -1,11 +1,9 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, - Transaction, captureException, continueTrace, defineIntegration, - getCurrentScope, setHttpStatus, startSpan, withIsolationScope, @@ -100,13 +98,10 @@ function instrumentBunServeOptions(serveOptions: Parameters[0] >); if (response && response.status) { setHttpStatus(span, response.status); - if (span instanceof Transaction) { - const scope = getCurrentScope(); - scope.setContext('response', { - headers: response.headers.toJSON(), - status_code: response.status, - }); - } + isolationScope.setContext('response', { + headers: response.headers.toJSON(), + status_code: response.status, + }); } return response; } catch (e) { diff --git a/packages/core/rollup.npm.config.mjs b/packages/core/rollup.npm.config.mjs index fd61fbf7c62c..d28a7a6f54a0 100644 --- a/packages/core/rollup.npm.config.mjs +++ b/packages/core/rollup.npm.config.mjs @@ -6,8 +6,11 @@ export default makeNPMConfigVariants( output: { // set exports to 'named' or 'auto' so that rollup doesn't warn exports: 'named', - // set preserveModules to false because we want to bundle everything into one file. - preserveModules: false, + // set preserveModules to true because we don't want to bundle everything into one file. + preserveModules: + process.env.SENTRY_BUILD_PRESERVE_MODULES === undefined + ? true + : Boolean(process.env.SENTRY_BUILD_PRESERVE_MODULES), }, }, }), diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index aa292e44cf33..454a37071205 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -53,6 +53,7 @@ import { setupIntegration, setupIntegrations } from './integration'; import type { Scope } from './scope'; import { updateSession } from './session'; import { getDynamicSamplingContextFromClient } from './tracing/dynamicSamplingContext'; +import { parseSampleRate } from './utils/parseSampleRate'; import { prepareEvent } from './utils/prepareEvent'; const ALREADY_SEEN_ERROR = "Not capturing exception because it's already been captured."; @@ -702,7 +703,8 @@ export abstract class BaseClient implements Client { // 1.0 === 100% events are sent // 0.0 === 0% events are sent // Sampling for transaction happens somewhere else - if (isError && typeof sampleRate === 'number' && Math.random() > sampleRate) { + const parsedSampleRate = typeof sampleRate === 'undefined' ? undefined : parseSampleRate(sampleRate); + if (isError && typeof parsedSampleRate === 'number' && Math.random() > parsedSampleRate) { this.recordDroppedEvent('sample_rate', 'error', event); return rejectedSyncPromise( new SentryError( diff --git a/packages/core/src/hub.ts b/packages/core/src/hub.ts index 4c6cecf6858b..de36626415ee 100644 --- a/packages/core/src/hub.ts +++ b/packages/core/src/hub.ts @@ -142,18 +142,6 @@ export class Hub implements HubInterface { this._isolationScope = assignedIsolationScope; } - /** - * Checks if this hub's version is older than the given version. - * - * @param version A version number to compare to. - * @return True if the given version is newer; otherwise false. - * - * @deprecated This will be removed in v8. - */ - public isOlderThan(version: number): boolean { - return this._version < version; - } - /** * This binds the given client to the current scope. * @param client An SDK client (client) instance. @@ -170,51 +158,19 @@ export class Hub implements HubInterface { } } - /** - * @inheritDoc - * - * @deprecated Use `withScope` instead. - */ - public pushScope(): ScopeInterface { - // We want to clone the content of prev scope - // eslint-disable-next-line deprecation/deprecation - const scope = this.getScope().clone(); - // eslint-disable-next-line deprecation/deprecation - this.getStack().push({ - // eslint-disable-next-line deprecation/deprecation - client: this.getClient(), - scope, - }); - return scope; - } - - /** - * @inheritDoc - * - * @deprecated Use `withScope` instead. - */ - public popScope(): boolean { - // eslint-disable-next-line deprecation/deprecation - if (this.getStack().length <= 1) return false; - // eslint-disable-next-line deprecation/deprecation - return !!this.getStack().pop(); - } - /** * @inheritDoc * * @deprecated Use `Sentry.withScope()` instead. */ public withScope(callback: (scope: ScopeInterface) => T): T { - // eslint-disable-next-line deprecation/deprecation - const scope = this.pushScope(); + const scope = this._pushScope(); let maybePromiseResult: T; try { maybePromiseResult = callback(scope); } catch (e) { - // eslint-disable-next-line deprecation/deprecation - this.popScope(); + this._popScope(); throw e; } @@ -222,20 +178,17 @@ export class Hub implements HubInterface { // @ts-expect-error - isThenable returns the wrong type return maybePromiseResult.then( res => { - // eslint-disable-next-line deprecation/deprecation - this.popScope(); + this._popScope(); return res; }, e => { - // eslint-disable-next-line deprecation/deprecation - this.popScope(); + this._popScope(); throw e; }, ); } - // eslint-disable-next-line deprecation/deprecation - this.popScope(); + this._popScope(); return maybePromiseResult; } @@ -501,20 +454,6 @@ export class Hub implements HubInterface { return session; } - /** - * Returns if default PII should be sent to Sentry and propagated in ourgoing requests - * when Tracing is used. - * - * @deprecated Use top-level `getClient().getOptions().sendDefaultPii` instead. This function - * only unnecessarily increased API surface but only wrapped accessing the option. - */ - public shouldSendDefaultPii(): boolean { - // eslint-disable-next-line deprecation/deprecation - const client = this.getClient(); - const options = client && client.getOptions(); - return Boolean(options && options.sendDefaultPii); - } - /** * Sends the current Session on the scope */ @@ -527,6 +466,32 @@ export class Hub implements HubInterface { client.captureSession(session); } } + + /** + * Push a scope to the stack. + */ + private _pushScope(): ScopeInterface { + // We want to clone the content of prev scope + // eslint-disable-next-line deprecation/deprecation + const scope = this.getScope().clone(); + // eslint-disable-next-line deprecation/deprecation + this.getStack().push({ + // eslint-disable-next-line deprecation/deprecation + client: this.getClient(), + scope, + }); + return scope; + } + + /** + * Pop a scope from the stack. + */ + private _popScope(): boolean { + // eslint-disable-next-line deprecation/deprecation + if (this.getStack().length <= 1) return false; + // eslint-disable-next-line deprecation/deprecation + return !!this.getStack().pop(); + } } /** diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3a5cab6986b3..48bb5baf6afc 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -65,8 +65,6 @@ export { getIntegrationsToSetup, addIntegration, defineIntegration, - // eslint-disable-next-line deprecation/deprecation - convertIntegrationFnToClass, } from './integration'; export { applyScopeDataToEvent, mergeScopeData } from './utils/applyScopeDataToEvent'; export { prepareEvent } from './utils/prepareEvent'; @@ -86,6 +84,7 @@ export { getActiveSpan, addChildSpanToSpan, } from './utils/spanUtils'; +export { parseSampleRate } from './utils/parseSampleRate'; export { applySdkMetadata } from './utils/sdkMetadata'; export { DEFAULT_ENVIRONMENT } from './constants'; export { addBreadcrumb } from './breadcrumbs'; @@ -106,3 +105,4 @@ export { metricsDefault } from './metrics/exports-default'; export { BrowserMetricsAggregator } from './metrics/browser-aggregator'; export { getMetricSummaryJsonForSpan } from './metrics/metric-summary'; export { addTracingHeadersToFetchRequest, instrumentFetchRequest } from './fetch'; +export { trpcMiddleware } from './trpc'; diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index 237a086b92bb..c5f9499f342e 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -1,4 +1,4 @@ -import type { Client, Event, EventHint, Integration, IntegrationClass, IntegrationFn, Options } from '@sentry/types'; +import type { Client, Event, EventHint, Integration, IntegrationFn, Options } from '@sentry/types'; import { arrayify, logger } from '@sentry/utils'; import { getClient } from './currentScopes'; @@ -169,24 +169,6 @@ function findIndex(arr: T[], callback: (item: T) => boolean): number { return -1; } -/** - * Convert a new integration function to the legacy class syntax. - * In v8, we can remove this and instead export the integration functions directly. - * - * @deprecated This will be removed in v8! - */ -export function convertIntegrationFnToClass( - name: string, - fn: Fn, -): IntegrationClass { - return Object.assign( - function ConvertedIntegration(...args: Parameters): Integration { - return fn(...args); - }, - { id: name }, - ) as unknown as IntegrationClass; -} - /** * Define an integration function that can be used to create an integration instance. * Note that this by design hides the implementation details of the integration, as they are considered internal. diff --git a/packages/core/src/integrations/requestdata.ts b/packages/core/src/integrations/requestdata.ts index e7c1e56dec82..23c3ae311533 100644 --- a/packages/core/src/integrations/requestdata.ts +++ b/packages/core/src/integrations/requestdata.ts @@ -1,4 +1,4 @@ -import type { Client, IntegrationFn, Transaction } from '@sentry/types'; +import type { Client, IntegrationFn, Span } from '@sentry/types'; import type { AddRequestDataToEventOptions, TransactionNamingScheme } from '@sentry/utils'; import { addRequestDataToEvent, extractPathForTransaction } from '@sentry/utils'; import { defineIntegration } from '../integration'; @@ -92,7 +92,7 @@ const _requestDataIntegration = ((options: RequestDataIntegrationOptions = {}) = // In all other cases, use the request's associated transaction (if any) to overwrite the event's `transaction` // value with a high-quality one - const reqWithTransaction = req as { _sentryTransaction?: Transaction }; + const reqWithTransaction = req as { _sentryTransaction?: Span }; const transaction = reqWithTransaction._sentryTransaction; if (transaction) { const name = spanToJSON(transaction).description || ''; diff --git a/packages/core/src/metrics/aggregator.ts b/packages/core/src/metrics/aggregator.ts index 169e40b42905..8b56d190b88a 100644 --- a/packages/core/src/metrics/aggregator.ts +++ b/packages/core/src/metrics/aggregator.ts @@ -20,7 +20,9 @@ export class MetricsAggregator implements MetricsAggregatorBase { // that we store in memory. private _bucketsTotalWeight; - private readonly _interval: ReturnType; + // Cast to any so that it can use Node.js timeout + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private readonly _interval: any; // SDKs are required to shift the flush interval by random() * rollup_in_seconds. // That shift is determined once per startup to create jittering. @@ -37,7 +39,13 @@ export class MetricsAggregator implements MetricsAggregatorBase { public constructor(private readonly _client: Client) { this._buckets = new Map(); this._bucketsTotalWeight = 0; - this._interval = setInterval(() => this._flush(), DEFAULT_FLUSH_INTERVAL); + + this._interval = setInterval(() => this._flush(), DEFAULT_FLUSH_INTERVAL) as any; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (this._interval.unref) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + this._interval.unref(); + } this._flushShift = Math.floor((Math.random() * DEFAULT_FLUSH_INTERVAL) / 1000); this._forceFlush = false; } diff --git a/packages/core/src/metrics/exports.ts b/packages/core/src/metrics/exports.ts index 2ed0d9cb9d51..4fb088287a40 100644 --- a/packages/core/src/metrics/exports.ts +++ b/packages/core/src/metrics/exports.ts @@ -5,10 +5,9 @@ import type { Primitive, } from '@sentry/types'; import { getGlobalSingleton, logger } from '@sentry/utils'; -import { getCurrentScope } from '../currentScopes'; import { getClient } from '../currentScopes'; import { DEBUG_BUILD } from '../debug-build'; -import { spanToJSON } from '../utils/spanUtils'; +import { getActiveSpan, getRootSpan, spanToJSON } from '../utils/spanUtils'; import { COUNTER_METRIC_TYPE, DISTRIBUTION_METRIC_TYPE, GAUGE_METRIC_TYPE, SET_METRIC_TYPE } from './constants'; import type { MetricType } from './types'; @@ -63,11 +62,11 @@ function addToMetricsAggregator( return; } - const scope = getCurrentScope(); + const span = getActiveSpan(); + const rootSpan = span ? getRootSpan(span) : undefined; + const { unit, tags, timestamp } = data; const { release, environment } = client.getOptions(); - // eslint-disable-next-line deprecation/deprecation - const transaction = scope.getTransaction(); const metricTags: Record = {}; if (release) { metricTags.release = release; @@ -75,8 +74,8 @@ function addToMetricsAggregator( if (environment) { metricTags.environment = environment; } - if (transaction) { - metricTags.transaction = spanToJSON(transaction).description || ''; + if (rootSpan) { + metricTags.transaction = spanToJSON(rootSpan).description || ''; } DEBUG_BUILD && logger.log(`Adding value of ${value} to ${metricType} metric ${name}`); diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index a86bbfafcc8f..0cd761113abd 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -19,13 +19,11 @@ import type { ScopeData, Session, SeverityLevel, - Transaction, User, } from '@sentry/types'; import { dateTimestampInSeconds, isPlainObject, logger, uuid4 } from '@sentry/utils'; import { updateSession } from './session'; -import type { SentrySpan } from './tracing/sentrySpan'; import { _getSpanForScope, _setSpanForScope } from './utils/spanOnScope'; /** @@ -112,14 +110,6 @@ export class Scope implements ScopeInterface { this._propagationContext = generatePropagationContext(); } - /** - * Inherit values from the parent scope. - * @deprecated Use `scope.clone()` and `new Scope()` instead. - */ - public static clone(scope?: Scope): Scope { - return scope ? scope.clone() : new Scope(); - } - /** * @inheritDoc */ @@ -302,25 +292,6 @@ export class Scope implements ScopeInterface { return this; } - /** - * Returns the `Transaction` attached to the scope (if there is one). - * @deprecated You should not rely on the transaction, but just use `startSpan()` APIs instead. - */ - public getTransaction(): Transaction | undefined { - // Often, this span (if it exists at all) will be a transaction, but it's not guaranteed to be. Regardless, it will - // have a pointer to the currently-active transaction. - const span = _getSpanForScope(this); - - // Cannot replace with getRootSpan because getRootSpan returns a span, not a transaction - // Also, this method will be removed anyway. - // eslint-disable-next-line deprecation/deprecation - if (span && (span as SentrySpan).transaction) { - // eslint-disable-next-line deprecation/deprecation - return (span as SentrySpan).transaction; - } - return undefined; - } - /** * @inheritDoc */ @@ -351,47 +322,37 @@ export class Scope implements ScopeInterface { const scopeToMerge = typeof captureContext === 'function' ? captureContext(this) : captureContext; - if (scopeToMerge instanceof Scope) { - const scopeData = scopeToMerge.getScopeData(); - - this._tags = { ...this._tags, ...scopeData.tags }; - this._extra = { ...this._extra, ...scopeData.extra }; - this._contexts = { ...this._contexts, ...scopeData.contexts }; - if (scopeData.user && Object.keys(scopeData.user).length) { - this._user = scopeData.user; - } - if (scopeData.level) { - this._level = scopeData.level; - } - if (scopeData.fingerprint.length) { - this._fingerprint = scopeData.fingerprint; - } - if (scopeToMerge.getRequestSession()) { - this._requestSession = scopeToMerge.getRequestSession(); - } - if (scopeData.propagationContext) { - this._propagationContext = scopeData.propagationContext; - } - } else if (isPlainObject(scopeToMerge)) { - const scopeContext = captureContext as ScopeContext; - this._tags = { ...this._tags, ...scopeContext.tags }; - this._extra = { ...this._extra, ...scopeContext.extra }; - this._contexts = { ...this._contexts, ...scopeContext.contexts }; - if (scopeContext.user) { - this._user = scopeContext.user; - } - if (scopeContext.level) { - this._level = scopeContext.level; - } - if (scopeContext.fingerprint) { - this._fingerprint = scopeContext.fingerprint; - } - if (scopeContext.requestSession) { - this._requestSession = scopeContext.requestSession; - } - if (scopeContext.propagationContext) { - this._propagationContext = scopeContext.propagationContext; - } + const [scopeInstance, requestSession] = + scopeToMerge instanceof Scope + ? [scopeToMerge.getScopeData(), scopeToMerge.getRequestSession()] + : isPlainObject(scopeToMerge) + ? [captureContext as ScopeContext, (captureContext as ScopeContext).requestSession] + : []; + + const { tags, extra, user, contexts, level, fingerprint = [], propagationContext } = scopeInstance || {}; + + this._tags = { ...this._tags, ...tags }; + this._extra = { ...this._extra, ...extra }; + this._contexts = { ...this._contexts, ...contexts }; + + if (user && Object.keys(user).length) { + this._user = user; + } + + if (level) { + this._level = level; + } + + if (fingerprint.length) { + this._fingerprint = fingerprint; + } + + if (propagationContext) { + this._propagationContext = propagationContext; + } + + if (requestSession) { + this._requestSession = requestSession; } return this; @@ -469,16 +430,6 @@ export class Scope implements ScopeInterface { return this; } - /** - * @inheritDoc - * @deprecated Use `getScopeData()` instead. - */ - public getAttachments(): Attachment[] { - const data = this.getScopeData(); - - return data.attachments; - } - /** * @inheritDoc */ @@ -489,34 +440,19 @@ export class Scope implements ScopeInterface { /** @inheritDoc */ public getScopeData(): ScopeData { - const { - _breadcrumbs, - _attachments, - _contexts, - _tags, - _extra, - _user, - _level, - _fingerprint, - _eventProcessors, - _propagationContext, - _sdkProcessingMetadata, - _transactionName, - } = this; - return { - breadcrumbs: _breadcrumbs, - attachments: _attachments, - contexts: _contexts, - tags: _tags, - extra: _extra, - user: _user, - level: _level, - fingerprint: _fingerprint || [], - eventProcessors: _eventProcessors, - propagationContext: _propagationContext, - sdkProcessingMetadata: _sdkProcessingMetadata, - transactionName: _transactionName, + breadcrumbs: this._breadcrumbs, + attachments: this._attachments, + contexts: this._contexts, + tags: this._tags, + extra: this._extra, + user: this._user, + level: this._level, + fingerprint: this._fingerprint || [], + eventProcessors: this._eventProcessors, + propagationContext: this._propagationContext, + sdkProcessingMetadata: this._sdkProcessingMetadata, + transactionName: this._transactionName, span: _getSpanForScope(this), }; } diff --git a/packages/core/src/semanticAttributes.ts b/packages/core/src/semanticAttributes.ts index 67ad231e7c10..aeb4dda815a2 100644 --- a/packages/core/src/semanticAttributes.ts +++ b/packages/core/src/semanticAttributes.ts @@ -22,3 +22,9 @@ export const SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN = 'sentry.origin'; /** The reason why an idle span finished. */ export const SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON = 'sentry.idle_span_finish_reason'; + +/** The unit of a measurement, which may be stored as a TimedEvent. */ +export const SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT = 'sentry.measurement_unit'; + +/** The value of a measurement, which may be stored as a TimedEvent. */ +export const SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE = 'sentry.measurement_value'; diff --git a/packages/core/src/sessionflusher.ts b/packages/core/src/sessionflusher.ts index 604a654a6b01..291864333119 100644 --- a/packages/core/src/sessionflusher.ts +++ b/packages/core/src/sessionflusher.ts @@ -20,7 +20,9 @@ export class SessionFlusher implements SessionFlusherLike { public readonly flushTimeout: number; private _pendingAggregates: Record; private _sessionAttrs: ReleaseHealthAttributes; - private _intervalId: ReturnType; + // Cast to any so that it can use Node.js timeout + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _intervalId: any; private _isEnabled: boolean; private _client: Client; @@ -30,8 +32,13 @@ export class SessionFlusher implements SessionFlusherLike { this._pendingAggregates = {}; this._isEnabled = true; - // Call to setInterval, so that flush is called every 60 seconds + // Call to setInterval, so that flush is called every 60 seconds. this._intervalId = setInterval(() => this.flush(), this.flushTimeout * 1000); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (this._intervalId.unref) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + this._intervalId.unref(); + } this._sessionAttrs = attrs; } diff --git a/packages/core/src/tracing/dynamicSamplingContext.ts b/packages/core/src/tracing/dynamicSamplingContext.ts index 04510683a1b9..2f69e9db6a2e 100644 --- a/packages/core/src/tracing/dynamicSamplingContext.ts +++ b/packages/core/src/tracing/dynamicSamplingContext.ts @@ -1,11 +1,29 @@ -import type { Client, DynamicSamplingContext, Span, Transaction } from '@sentry/types'; -import { dropUndefinedKeys } from '@sentry/utils'; +import type { Client, DynamicSamplingContext, Span } from '@sentry/types'; +import { addNonEnumerableProperty, dropUndefinedKeys } from '@sentry/utils'; import { DEFAULT_ENVIRONMENT } from '../constants'; import { getClient } from '../currentScopes'; -import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../semanticAttributes'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../semanticAttributes'; import { getRootSpan, spanIsSampled, spanToJSON } from '../utils/spanUtils'; +/** + * If you change this value, also update the terser plugin config to + * avoid minification of the object property! + */ +const FROZEN_DSC_FIELD = '_frozenDsc'; + +type SpanWithMaybeDsc = Span & { + [FROZEN_DSC_FIELD]?: Partial | undefined; +}; + +/** + * Freeze the given DSC on the given span. + */ +export function freezeDscOnSpan(span: Span, dsc: Partial): void { + const spanWithMaybeDsc = span as SpanWithMaybeDsc; + addNonEnumerableProperty(spanWithMaybeDsc, FROZEN_DSC_FIELD, dsc); +} + /** * Creates a dynamic sampling context from a client. * @@ -28,11 +46,6 @@ export function getDynamicSamplingContextFromClient(trace_id: string, client: Cl return dsc; } -/** - * A Span with a frozen dynamic sampling context. - */ -type TransactionWithV7FrozenDsc = Transaction & { _frozenDynamicSamplingContext?: DynamicSamplingContext }; - /** * Creates a dynamic sampling context from a span (and client and scope) * @@ -48,32 +61,26 @@ export function getDynamicSamplingContextFromSpan(span: Span): Readonly child !== span); @@ -259,7 +254,7 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti childSpan.setStatus({ code: SPAN_STATUS_ERROR, message: 'cancelled' }); childSpan.end(endTimestamp); DEBUG_BUILD && - logger.log('[Tracing] cancelling span since span ended early', JSON.stringify(childSpan, undefined, 2)); + logger.log('[Tracing] Cancelling span since span ended early', JSON.stringify(childSpan, undefined, 2)); } const childSpanJSON = spanToJSON(childSpan); @@ -274,9 +269,9 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti if (DEBUG_BUILD) { const stringifiedSpan = JSON.stringify(childSpan, undefined, 2); if (!spanStartedBeforeIdleSpanEnd) { - logger.log('[Tracing] discarding Span since it happened after idle span was finished', stringifiedSpan); + logger.log('[Tracing] Discarding span since it happened after idle span was finished', stringifiedSpan); } else if (!spanEndedBeforeFinalTimeout) { - logger.log('[Tracing] discarding Span since it finished after idle span final timeout', stringifiedSpan); + logger.log('[Tracing] Discarding span since it finished after idle span final timeout', stringifiedSpan); } } @@ -284,8 +279,6 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti removeChildSpanFromSpan(span, childSpan); } }); - - DEBUG_BUILD && logger.log('[Tracing] flushing idle span'); } client.on('spanStart', startedSpan => { @@ -349,7 +342,7 @@ function _startIdleSpan(options: StartSpanOptions): Span { _setSpanForScope(getCurrentScope(), span); - DEBUG_BUILD && logger.log(`Setting idle span on scope. Span ID: ${span.spanContext().spanId}`); + DEBUG_BUILD && logger.log('[Tracing] Started span is an idle span'); return span; } diff --git a/packages/core/src/tracing/index.ts b/packages/core/src/tracing/index.ts index c8e38bd9095a..cd9ca5ea6351 100644 --- a/packages/core/src/tracing/index.ts +++ b/packages/core/src/tracing/index.ts @@ -2,7 +2,6 @@ export { addTracingExtensions } from './hubextensions'; export { startIdleSpan, TRACING_DEFAULTS } from './idleSpan'; export { SentrySpan } from './sentrySpan'; export { SentryNonRecordingSpan } from './sentryNonRecordingSpan'; -export { Transaction } from './transaction'; export { setHttpStatus, getSpanStatusFromHttpCode, @@ -16,4 +15,6 @@ export { withActiveSpan, } from './trace'; export { getDynamicSamplingContextFromClient, getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; -export { setMeasurement } from './measurement'; +export { setMeasurement, timedEventsToMeasurements } from './measurement'; +export { sampleSpan } from './sampling'; +export { logSpanEnd, logSpanStart } from './logSpans'; diff --git a/packages/core/src/tracing/logSpans.ts b/packages/core/src/tracing/logSpans.ts new file mode 100644 index 000000000000..11a83c6b7a41 --- /dev/null +++ b/packages/core/src/tracing/logSpans.ts @@ -0,0 +1,55 @@ +import type { Span } from '@sentry/types'; +import { logger } from '@sentry/utils'; +import { DEBUG_BUILD } from '../debug-build'; +import { getRootSpan, spanIsSampled, spanToJSON } from '../utils/spanUtils'; + +/** + * Print a log message for a started span. + */ +export function logSpanStart(span: Span): void { + if (!DEBUG_BUILD) return; + + const { description = '< unknown name >', op = '< unknown op >', parent_span_id: parentSpanId } = spanToJSON(span); + const { spanId } = span.spanContext(); + + const sampled = spanIsSampled(span); + const rootSpan = getRootSpan(span); + const isRootSpan = rootSpan === span; + + const header = `[Tracing] Starting ${sampled ? 'sampled' : 'unsampled'} ${isRootSpan ? 'root ' : ''}span`; + + const infoParts: string[] = [`op: ${op}`, `name: ${description}`, `ID: ${spanId}`]; + + if (parentSpanId) { + infoParts.push(`parent ID: ${parentSpanId}`); + } + + if (!isRootSpan) { + const { op, description } = spanToJSON(rootSpan); + infoParts.push(`root ID: ${rootSpan.spanContext().spanId}`); + if (op) { + infoParts.push(`root op: ${op}`); + } + if (description) { + infoParts.push(`root description: ${description}`); + } + } + + logger.log(`${header} + ${infoParts.join('\n ')}`); +} + +/** + * Print a log message for an ended span. + */ +export function logSpanEnd(span: Span): void { + if (!DEBUG_BUILD) return; + + const { description = '< unknown name >', op = '< unknown op >' } = spanToJSON(span); + const { spanId } = span.spanContext(); + const rootSpan = getRootSpan(span); + const isRootSpan = rootSpan === span; + + const msg = `[Tracing] Finishing "${op}" ${isRootSpan ? 'root ' : ''}span "${description}" with ID ${spanId}`; + logger.log(msg); +} diff --git a/packages/core/src/tracing/measurement.ts b/packages/core/src/tracing/measurement.ts index 6945bba8aec8..a8328387c209 100644 --- a/packages/core/src/tracing/measurement.ts +++ b/packages/core/src/tracing/measurement.ts @@ -1,4 +1,8 @@ -import type { MeasurementUnit, Span, Transaction } from '@sentry/types'; +import type { MeasurementUnit, Measurements, TimedEvent } from '@sentry/types'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT, + SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE, +} from '../semanticAttributes'; import { getActiveSpan, getRootSpan } from '../utils/spanUtils'; /** @@ -8,13 +12,28 @@ export function setMeasurement(name: string, value: number, unit: MeasurementUni const activeSpan = getActiveSpan(); const rootSpan = activeSpan && getRootSpan(activeSpan); - if (rootSpan && rootSpanIsTransaction(rootSpan)) { - // eslint-disable-next-line deprecation/deprecation - rootSpan.setMeasurement(name, value, unit); + if (rootSpan) { + rootSpan.addEvent(name, { + [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE]: value, + [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT]: unit as string, + }); } } -function rootSpanIsTransaction(rootSpan: Span): rootSpan is Transaction { - // eslint-disable-next-line deprecation/deprecation - return typeof (rootSpan as Transaction).setMeasurement === 'function'; +/** + * Convert timed events to measurements. + */ +export function timedEventsToMeasurements(events: TimedEvent[]): Measurements { + const measurements: Measurements = {}; + events.forEach(event => { + const attributes = event.attributes || {}; + const unit = attributes[SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT] as MeasurementUnit | undefined; + const value = attributes[SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE] as number | undefined; + + if (typeof unit === 'string' && typeof value === 'number') { + measurements[event.name] = { value, unit }; + } + }); + + return measurements; } diff --git a/packages/core/src/tracing/sampling.ts b/packages/core/src/tracing/sampling.ts index ce269e55a27d..1f8e0d0eda83 100644 --- a/packages/core/src/tracing/sampling.ts +++ b/packages/core/src/tracing/sampling.ts @@ -1,20 +1,17 @@ -import type { Options, SamplingContext, TransactionContext } from '@sentry/types'; -import { isNaN, logger } from '@sentry/utils'; +import type { Options, SamplingContext } from '@sentry/types'; +import { logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; import { hasTracingEnabled } from '../utils/hasTracingEnabled'; +import { parseSampleRate } from '../utils/parseSampleRate'; /** - * Makes a sampling decision for the given transaction and stores it on the transaction. + * Makes a sampling decision for the given options. * - * Called every time a transaction is created. Only transactions which emerge with a `sampled` value of `true` will be + * Called every time a root span is created. Only root spans which emerge with a `sampled` value of `true` will be * sent to Sentry. - * - * This method muttes the given `transaction` and will set the `sampled` value on it. - * It returns the same transaction, for convenience. */ -export function sampleTransaction( - transactionContext: TransactionContext, +export function sampleSpan( options: Pick, samplingContext: SamplingContext, ): [sampled: boolean, sampleRate?: number] { @@ -23,13 +20,6 @@ export function sampleTransaction( return [false]; } - const transactionContextSampled = transactionContext.sampled; - // if the user has forced a sampling decision by passing a `sampled` value in - // their transaction context, go with that. - if (transactionContextSampled !== undefined) { - return [transactionContextSampled, Number(transactionContextSampled)]; - } - // we would have bailed already if neither `tracesSampler` nor `tracesSampleRate` nor `enableTracing` were defined, so one of these should // work; prefer the hook if so let sampleRate; @@ -44,15 +34,17 @@ export function sampleTransaction( sampleRate = 1; } - // Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The - // only valid values are booleans or numbers between 0 and 1.) - if (!isValidSampleRate(sampleRate)) { + // Since this is coming from the user (or from a function provided by the user), who knows what we might get. + // (The only valid values are booleans or numbers between 0 and 1.) + const parsedSampleRate = parseSampleRate(sampleRate); + + if (parsedSampleRate === undefined) { DEBUG_BUILD && logger.warn('[Tracing] Discarding transaction because of invalid sample rate.'); return [false]; } // if the function returned 0 (or false), or if `tracesSampleRate` is 0, it's a sign the transaction should be dropped - if (!sampleRate) { + if (!parsedSampleRate) { DEBUG_BUILD && logger.log( `[Tracing] Discarding transaction because ${ @@ -61,12 +53,12 @@ export function sampleTransaction( : 'a negative sampling decision was inherited or tracesSampleRate is set to 0' }`, ); - return [false, Number(sampleRate)]; + return [false, parsedSampleRate]; } // Now we roll the dice. Math.random is inclusive of 0, but not of 1, so strict < is safe here. In case sampleRate is // a boolean, the < comparison will cause it to be automatically cast to 1 if it's true and 0 if it's false. - const shouldSample = Math.random() < sampleRate; + const shouldSample = Math.random() < parsedSampleRate; // if we're not going to keep it, we're done if (!shouldSample) { @@ -76,32 +68,8 @@ export function sampleTransaction( sampleRate, )})`, ); - return [false, Number(sampleRate)]; - } - - return [true, Number(sampleRate)]; -} - -/** - * Checks the given sample rate to make sure it is valid type and value (a boolean, or a number between 0 and 1). - */ -function isValidSampleRate(rate: unknown): rate is number | boolean { - // we need to check NaN explicitly because it's of type 'number' and therefore wouldn't get caught by this typecheck - if (isNaN(rate) || !(typeof rate === 'number' || typeof rate === 'boolean')) { - DEBUG_BUILD && - logger.warn( - `[Tracing] Given sample rate is invalid. Sample rate must be a boolean or a number between 0 and 1. Got ${JSON.stringify( - rate, - )} of type ${JSON.stringify(typeof rate)}.`, - ); - return false; + return [false, parsedSampleRate]; } - // in case sampleRate is a boolean, it will get automatically cast to 1 if it's true and 0 if it's false - if (rate < 0 || rate > 1) { - DEBUG_BUILD && - logger.warn(`[Tracing] Given sample rate is invalid. Sample rate must be between 0 and 1. Got ${rate}.`); - return false; - } - return true; + return [true, parsedSampleRate]; } diff --git a/packages/core/src/tracing/sentryNonRecordingSpan.ts b/packages/core/src/tracing/sentryNonRecordingSpan.ts index 6b86fe4a0fec..1debb45aa282 100644 --- a/packages/core/src/tracing/sentryNonRecordingSpan.ts +++ b/packages/core/src/tracing/sentryNonRecordingSpan.ts @@ -1,8 +1,8 @@ import type { + SentrySpanArguments, Span, SpanAttributeValue, SpanAttributes, - SpanContext, SpanContextData, SpanStatus, SpanTimeInput, @@ -17,7 +17,7 @@ export class SentryNonRecordingSpan implements Span { private _traceId: string; private _spanId: string; - public constructor(spanContext: SpanContext = {}) { + public constructor(spanContext: SentrySpanArguments = {}) { this._traceId = spanContext.traceId || uuid4(); this._spanId = spanContext.spanId || uuid4().substring(16); } @@ -59,4 +59,13 @@ export class SentryNonRecordingSpan implements Span { public isRecording(): boolean { return false; } + + /** @inheritdoc */ + public addEvent( + _name: string, + _attributesOrStartTime?: SpanAttributes | SpanTimeInput, + _startTime?: SpanTimeInput, + ): this { + return this; + } } diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index 2eceafafa953..c7d436ae3c0a 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -1,49 +1,46 @@ import type { + SentrySpanArguments, Span, SpanAttributeValue, SpanAttributes, - SpanContext, SpanContextData, SpanJSON, SpanOrigin, SpanStatus, SpanTimeInput, - TraceContext, - Transaction, + TimedEvent, + TransactionEvent, + TransactionSource, } from '@sentry/types'; import { dropUndefinedKeys, logger, timestampInSeconds, uuid4 } from '@sentry/utils'; -import { getClient } from '../currentScopes'; - +import { getClient, getCurrentScope } from '../currentScopes'; import { DEBUG_BUILD } from '../debug-build'; + import { getMetricSummaryJsonForSpan } from '../metrics/metric-summary'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, +} from '../semanticAttributes'; import { TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED, - addChildSpanToSpan, getRootSpan, + getSpanDescendants, getStatusMessage, spanTimeInputToSeconds, spanToJSON, spanToTraceContext, } from '../utils/spanUtils'; +import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; +import { logSpanEnd } from './logSpans'; +import { timedEventsToMeasurements } from './measurement'; +import { getCapturedScopesOnSpan } from './utils'; /** * Span contains all data about a span */ export class SentrySpan implements Span { - /** - * Data for the span. - * @deprecated Use `spanToJSON(span).atttributes` instead. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public data: { [key: string]: any }; - - /** - * @inheritDoc - * @deprecated Use top level `Sentry.getRootSpan()` instead - */ - public transaction?: Transaction; protected _traceId: string; protected _spanId: string; protected _parentSpanId?: string | undefined; @@ -56,8 +53,8 @@ export class SentrySpan implements Span { protected _endTime?: number | undefined; /** Internal keeper of the status */ protected _status?: SpanStatus; - - private _logMessage?: string; + /** The timed events added to this span. */ + protected _events: TimedEvent[]; /** * You should never call the constructor manually, always use `Sentry.startSpan()` @@ -66,12 +63,10 @@ export class SentrySpan implements Span { * @hideconstructor * @hidden */ - public constructor(spanContext: SpanContext = {}) { + public constructor(spanContext: SentrySpanArguments = {}) { this._traceId = spanContext.traceId || uuid4(); this._spanId = spanContext.spanId || uuid4().substring(16); this._startTime = spanContext.startTimestamp || timestampInSeconds(); - // eslint-disable-next-line deprecation/deprecation - this.data = spanContext.data ? { ...spanContext.data } : {}; this._attributes = {}; this.setAttributes({ @@ -92,61 +87,15 @@ export class SentrySpan implements Span { if (spanContext.endTimestamp) { this._endTime = spanContext.endTimestamp; } - } - - // This rule conflicts with another eslint rule :( - /* eslint-disable @typescript-eslint/member-ordering */ - /** - * Attributes for the span. - * @deprecated Use `spanToJSON(span).atttributes` instead. - */ - public get attributes(): SpanAttributes { - return this._attributes; - } - - /** - * Attributes for the span. - * @deprecated Use `setAttributes()` instead. - */ - public set attributes(attributes: SpanAttributes) { - this._attributes = attributes; - } - - /** - * Timestamp in seconds (epoch time) indicating when the span started. - * @deprecated Use `spanToJSON()` instead. - */ - public get startTimestamp(): number { - return this._startTime; - } - - /** - * Timestamp in seconds (epoch time) indicating when the span started. - * @deprecated In v8, you will not be able to update the span start time after creation. - */ - public set startTimestamp(startTime: number) { - this._startTime = startTime; - } - - /** - * Timestamp in seconds when the span ended. - * @deprecated Use `spanToJSON()` instead. - */ - public get endTimestamp(): number | undefined { - return this._endTime; - } + this._events = []; - /** - * Timestamp in seconds when the span ended. - * @deprecated Set the end time via `span.end()` instead. - */ - public set endTimestamp(endTime: number | undefined) { - this._endTime = endTime; + // If the span is already ended, ensure we finalize the span immediately + if (this._endTime) { + this._onSpanEnded(); + } } - /* eslint-enable @typescript-eslint/member-ordering */ - /** @inheritdoc */ public spanContext(): SpanContextData { const { _spanId: spanId, _traceId: traceId, _sampled: sampled } = this; @@ -157,68 +106,6 @@ export class SentrySpan implements Span { }; } - /** - * Creates a new `Span` while setting the current `Span.id` as `parentSpanId`. - * Also the `sampled` decision will be inherited. - * - * @deprecated Use `startSpan()`, `startSpanManual()` or `startInactiveSpan()` instead. - */ - public startChild( - spanContext: Pick> = {}, - ): Span { - const childSpan = new SentrySpan({ - ...spanContext, - parentSpanId: this._spanId, - sampled: this._sampled, - traceId: this._traceId, - }); - - // To allow for interoperability we track the children of a span twice: Once with the span recorder (old) once with - // the `addChildSpanToSpan`. Eventually we will only use `addChildSpanToSpan` and drop the span recorder. - // To ensure interoperability with the `startSpan` API, `addChildSpanToSpan` is also called here. - addChildSpanToSpan(this, childSpan); - - const rootSpan = getRootSpan(this); - // TODO: still set span.transaction here until we have a more permanent solution - // Probably similarly to the weakmap we hold in node-experimental - // eslint-disable-next-line deprecation/deprecation - childSpan.transaction = rootSpan as Transaction; - - if (DEBUG_BUILD && rootSpan) { - const opStr = (spanContext && spanContext.op) || '< unknown op >'; - const nameStr = spanToJSON(childSpan).description || '< unknown name >'; - const idStr = rootSpan.spanContext().spanId; - - const logMessage = `[Tracing] Starting '${opStr}' span on transaction '${nameStr}' (${idStr}).`; - logger.log(logMessage); - this._logMessage = logMessage; - } - - const client = getClient(); - if (client) { - client.emit('spanStart', childSpan); - // If it has an endTimestamp, it's already ended - if (spanContext.endTimestamp) { - client.emit('spanEnd', childSpan); - } - } - - return childSpan; - } - - /** - * Sets the data attribute on the current span - * @param key Data key - * @param value Data value - * @deprecated Use `setAttribute()` instead. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public setData(key: string, value: any): this { - // eslint-disable-next-line deprecation/deprecation - this.data = { ...this.data, [key]: value }; - return this; - } - /** @inheritdoc */ public setAttribute(key: string, value: SpanAttributeValue | undefined): void { if (value === undefined) { @@ -268,53 +155,13 @@ export class SentrySpan implements Span { if (this._endTime) { return; } - const rootSpan = getRootSpan(this); - if ( - DEBUG_BUILD && - // Don't call this for transactions - rootSpan && - rootSpan.spanContext().spanId !== this._spanId - ) { - const logMessage = this._logMessage; - if (logMessage) { - logger.log((logMessage as string).replace('Starting', 'Finishing')); - } - } this._endTime = spanTimeInputToSeconds(endTimestamp); + logSpanEnd(this); this._onSpanEnded(); } - /** - * @inheritDoc - * - * @deprecated Use `spanToJSON()` or access the fields directly instead. - */ - public toContext(): SpanContext { - return dropUndefinedKeys({ - data: this._getData(), - name: this._name, - endTimestamp: this._endTime, - op: this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP], - parentSpanId: this._parentSpanId, - sampled: this._sampled, - spanId: this._spanId, - startTimestamp: this._startTime, - status: this._status, - traceId: this._traceId, - }); - } - - /** - * @inheritDoc - * - * @deprecated Use `spanToTraceContext()` util function instead. - */ - public getTraceContext(): TraceContext { - return spanToTraceContext(this); - } - /** * Get JSON representation of this span. * @@ -325,7 +172,7 @@ export class SentrySpan implements Span { */ public getSpanJSON(): SpanJSON { return dropUndefinedKeys({ - data: this._getData(), + data: this._attributes, description: this._name, op: this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP], parent_span_id: this._parentSpanId, @@ -345,49 +192,124 @@ export class SentrySpan implements Span { } /** - * Convert the object to JSON. - * @deprecated Use `spanToJSON(span)` instead. + * @inheritdoc */ - public toJSON(): SpanJSON { - return this.getSpanJSON(); + public addEvent( + name: string, + attributesOrStartTime?: SpanAttributes | SpanTimeInput, + startTime?: SpanTimeInput, + ): this { + DEBUG_BUILD && logger.log('[Tracing] Adding an event to span:', name); + + const time = isSpanTimeInput(attributesOrStartTime) ? attributesOrStartTime : startTime || timestampInSeconds(); + const attributes = isSpanTimeInput(attributesOrStartTime) ? {} : attributesOrStartTime || {}; + + const event: TimedEvent = { + name, + time: spanTimeInputToSeconds(time), + attributes, + }; + + this._events.push(event); + + return this; + } + + /** Emit `spanEnd` when the span is ended. */ + private _onSpanEnded(): void { + const client = getClient(); + if (client) { + client.emit('spanEnd', this); + } + + // If this is a root span, send it when it is endedf + if (this === getRootSpan(this)) { + const transactionEvent = this._convertSpanToTransaction(); + if (transactionEvent) { + const scope = getCapturedScopesOnSpan(this).scope || getCurrentScope(); + scope.captureEvent(transactionEvent); + } + } } /** - * Get the merged data for this span. - * For now, this combines `data` and `attributes` together, - * until eventually we can ingest `attributes` directly. + * Finish the transaction & prepare the event to send to Sentry. */ - private _getData(): - | { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key: string]: any; - } - | undefined { - // eslint-disable-next-line deprecation/deprecation - const { data, _attributes: attributes } = this; + private _convertSpanToTransaction(): TransactionEvent | undefined { + // We can only convert finished spans + if (!isFullFinishedSpan(spanToJSON(this))) { + return undefined; + } - const hasData = Object.keys(data).length > 0; - const hasAttributes = Object.keys(attributes).length > 0; + if (!this._name) { + DEBUG_BUILD && logger.warn('Transaction has no name, falling back to ``.'); + this._name = ''; + } + + const { scope: capturedSpanScope, isolationScope: capturedSpanIsolationScope } = getCapturedScopesOnSpan(this); + const scope = capturedSpanScope || getCurrentScope(); + const client = scope.getClient() || getClient(); + + if (this._sampled !== true) { + // At this point if `sampled !== true` we want to discard the transaction. + DEBUG_BUILD && logger.log('[Tracing] Discarding transaction because its trace was not chosen to be sampled.'); + + if (client) { + client.recordDroppedEvent('sample_rate', 'transaction'); + } - if (!hasData && !hasAttributes) { return undefined; } - if (hasData && hasAttributes) { - return { - ...data, - ...attributes, - }; - } + // The transaction span itself should be filtered out + const finishedSpans = getSpanDescendants(this).filter(span => span !== this); - return hasData ? data : attributes; - } + const spans = finishedSpans.map(span => spanToJSON(span)).filter(isFullFinishedSpan); - /** Emit `spanEnd` when the span is ended. */ - private _onSpanEnded(): void { - const client = getClient(); - if (client) { - client.emit('spanEnd', this); + const source = this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] as TransactionSource | undefined; + + const transaction: TransactionEvent = { + contexts: { + trace: spanToTraceContext(this), + }, + spans, + start_timestamp: this._startTime, + timestamp: this._endTime, + transaction: this._name, + type: 'transaction', + sdkProcessingMetadata: { + capturedSpanScope, + capturedSpanIsolationScope, + ...dropUndefinedKeys({ + dynamicSamplingContext: getDynamicSamplingContextFromSpan(this), + }), + }, + _metrics_summary: getMetricSummaryJsonForSpan(this), + ...(source && { + transaction_info: { + source, + }, + }), + }; + + const measurements = timedEventsToMeasurements(this._events); + const hasMeasurements = Object.keys(measurements).length; + + if (hasMeasurements) { + DEBUG_BUILD && + logger.log('[Measurements] Adding measurements to transaction', JSON.stringify(measurements, undefined, 2)); + transaction.measurements = measurements; } + + return transaction; } } + +function isSpanTimeInput(value: undefined | SpanAttributes | SpanTimeInput): value is SpanTimeInput { + return (value && typeof value === 'number') || value instanceof Date || Array.isArray(value); +} + +// We want to filter out any incomplete SpanJSON objects +function isFullFinishedSpan(input: Partial): input is SpanJSON { + return !!input.start_timestamp && !!input.timestamp && !!input.span_id && !!input.trace_id; +} diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index e4102b610cc1..d21a567cecc5 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -1,12 +1,12 @@ -import type { ClientOptions, Scope, Span, SpanTimeInput, StartSpanOptions, TransactionContext } from '@sentry/types'; +import type { ClientOptions, Scope, SentrySpanArguments, Span, SpanTimeInput, StartSpanOptions } from '@sentry/types'; import { propagationContextFromHeaders } from '@sentry/utils'; import type { AsyncContextStrategy } from '../asyncContext'; import { getMainCarrier } from '../asyncContext'; import { getClient, getCurrentScope, getIsolationScope, withScope } from '../currentScopes'; -import { getAsyncContextStrategy, getCurrentHub } from '../hub'; -import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '../semanticAttributes'; +import { getAsyncContextStrategy } from '../hub'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../semanticAttributes'; import { handleCallbackErrors } from '../utils/handleCallbackErrors'; import { hasTracingEnabled } from '../utils/hasTracingEnabled'; import { _getSpanForScope, _setSpanForScope } from '../utils/spanOnScope'; @@ -17,12 +17,12 @@ import { spanTimeInputToSeconds, spanToJSON, } from '../utils/spanUtils'; -import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; -import { sampleTransaction } from './sampling'; +import { freezeDscOnSpan, getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; +import { logSpanStart } from './logSpans'; +import { sampleSpan } from './sampling'; import { SentryNonRecordingSpan } from './sentryNonRecordingSpan'; -import type { SentrySpan } from './sentrySpan'; +import { SentrySpan } from './sentrySpan'; import { SPAN_STATUS_ERROR } from './spanstatus'; -import { Transaction } from './transaction'; import { setCapturedScopesOnSpan } from './utils'; /** @@ -211,7 +211,7 @@ function createChildSpanOrTransaction({ scope, }: { parentSpan: SentrySpan | undefined; - spanContext: TransactionContext; + spanContext: SentrySpanArguments; forceTransaction?: boolean; scope: Scope; }): Span { @@ -223,67 +223,66 @@ function createChildSpanOrTransaction({ let span: Span; if (parentSpan && !forceTransaction) { - // eslint-disable-next-line deprecation/deprecation - span = parentSpan.startChild(spanContext); + span = _startChildSpan(parentSpan, spanContext); addChildSpanToSpan(parentSpan, span); } else if (parentSpan) { // If we forced a transaction but have a parent span, make sure to continue from the parent span, not the scope const dsc = getDynamicSamplingContextFromSpan(parentSpan); const { traceId, spanId: parentSpanId } = parentSpan.spanContext(); - const sampled = spanIsSampled(parentSpan); + const parentSampled = spanIsSampled(parentSpan); - span = _startTransaction({ - traceId, - parentSpanId, - parentSampled: sampled, - ...spanContext, - metadata: { - dynamicSamplingContext: dsc, - // eslint-disable-next-line deprecation/deprecation - ...spanContext.metadata, + span = _startRootSpan( + { + traceId, + parentSpanId, + ...spanContext, }, - }); + parentSampled, + ); + + freezeDscOnSpan(span, dsc); } else { - const { traceId, dsc, parentSpanId, sampled } = { + const { + traceId, + dsc, + parentSpanId, + sampled: parentSampled, + } = { ...isolationScope.getPropagationContext(), ...scope.getPropagationContext(), }; - span = _startTransaction({ - traceId, - parentSpanId, - parentSampled: sampled, - ...spanContext, - metadata: { - dynamicSamplingContext: dsc, - // eslint-disable-next-line deprecation/deprecation - ...spanContext.metadata, + span = _startRootSpan( + { + traceId, + parentSpanId, + ...spanContext, }, - }); - } + parentSampled, + ); - // TODO v8: Technically `startTransaction` can return undefined, which is not reflected by the types - // This happens if tracing extensions have not been added - // In this case, we just want to return a non-recording span - if (!span) { - return new SentryNonRecordingSpan(); + if (dsc) { + freezeDscOnSpan(span, dsc); + } } + logSpanStart(span); + setCapturedScopesOnSpan(span, scope, isolationScope); return span; } /** - * This converts StartSpanOptions to TransactionContext. + * This converts StartSpanOptions to SentrySpanArguments. * For the most part (for now) we accept the same options, * but some of them need to be transformed. * * Eventually the StartSpanOptions will be more aligned with OpenTelemetry. */ -function normalizeContext(context: StartSpanOptions): TransactionContext { +function normalizeContext(context: StartSpanOptions): SentrySpanArguments { if (context.startTime) { - const ctx: TransactionContext & { startTime?: SpanTimeInput } = { ...context }; + const ctx: SentrySpanArguments & { startTime?: SpanTimeInput } = { ...context }; ctx.startTimestamp = spanTimeInputToSeconds(context.startTime); delete ctx.startTime; return ctx; @@ -297,23 +296,29 @@ function getAcs(): AsyncContextStrategy { return getAsyncContextStrategy(carrier); } -function _startTransaction(transactionContext: TransactionContext): Transaction { +function _startRootSpan(spanArguments: SentrySpanArguments, parentSampled?: boolean): SentrySpan { const client = getClient(); const options: Partial = (client && client.getOptions()) || {}; - const [sampled, sampleRate] = sampleTransaction(transactionContext, options, { - name: transactionContext.name, - parentSampled: transactionContext.parentSampled, - transactionContext, - attributes: { - // eslint-disable-next-line deprecation/deprecation - ...transactionContext.data, - ...transactionContext.attributes, + const { name = '', attributes } = spanArguments; + const [sampled, sampleRate] = sampleSpan(options, { + name, + parentSampled, + attributes, + transactionContext: { + name, + parentSampled, }, }); - // eslint-disable-next-line deprecation/deprecation - const transaction = new Transaction({ ...transactionContext, sampled }, getCurrentHub()); + const transaction = new SentrySpan({ + ...spanArguments, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + ...spanArguments.attributes, + }, + sampled, + }); if (sampleRate !== undefined) { transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, sampleRate); } @@ -324,3 +329,32 @@ function _startTransaction(transactionContext: TransactionContext): Transaction return transaction; } + +/** + * Creates a new `Span` while setting the current `Span.id` as `parentSpanId`. + * This inherits the sampling decision from the parent span. + */ +function _startChildSpan(parentSpan: Span, spanArguments: SentrySpanArguments): SentrySpan { + const { spanId, traceId } = parentSpan.spanContext(); + const sampled = spanIsSampled(parentSpan); + + const childSpan = new SentrySpan({ + ...spanArguments, + parentSpanId: spanId, + traceId, + sampled, + }); + + addChildSpanToSpan(parentSpan, childSpan); + + const client = getClient(); + if (client) { + client.emit('spanStart', childSpan); + // If it has an endTimestamp, it's already ended + if (spanArguments.endTimestamp) { + client.emit('spanEnd', childSpan); + } + } + + return childSpan; +} diff --git a/packages/core/src/tracing/transaction.ts b/packages/core/src/tracing/transaction.ts deleted file mode 100644 index 2469594ad687..000000000000 --- a/packages/core/src/tracing/transaction.ts +++ /dev/null @@ -1,301 +0,0 @@ -import type { - Context, - Contexts, - DynamicSamplingContext, - Hub, - MeasurementUnit, - Measurements, - SpanJSON, - SpanTimeInput, - Transaction as TransactionInterface, - TransactionContext, - TransactionEvent, - TransactionMetadata, - TransactionSource, -} from '@sentry/types'; -import { dropUndefinedKeys, logger } from '@sentry/utils'; - -import { DEBUG_BUILD } from '../debug-build'; -import { getCurrentHub } from '../hub'; -import { getMetricSummaryJsonForSpan } from '../metrics/metric-summary'; -import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../semanticAttributes'; -import { getSpanDescendants, spanTimeInputToSeconds, spanToJSON, spanToTraceContext } from '../utils/spanUtils'; -import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; -import { SentrySpan } from './sentrySpan'; -import { getCapturedScopesOnSpan } from './utils'; - -/** JSDoc */ -export class Transaction extends SentrySpan implements TransactionInterface { - /** - * The reference to the current hub. - */ - public _hub: Hub; - - protected _name: string; - - private _measurements: Measurements; - - private _contexts: Contexts; - - private _trimEnd?: boolean | undefined; - - // DO NOT yet remove this property, it is used in a hack for v7 backwards compatibility. - private _frozenDynamicSamplingContext: Readonly> | undefined; - - private _metadata: Partial; - - /** - * This constructor should never be called manually. - * @internal - * @hideconstructor - * @hidden - * - * @deprecated Transactions will be removed in v8. Use spans instead. - */ - public constructor(transactionContext: TransactionContext, hub?: Hub) { - super(transactionContext); - this._measurements = {}; - this._contexts = {}; - - // eslint-disable-next-line deprecation/deprecation - this._hub = hub || getCurrentHub(); - - this._name = transactionContext.name || ''; - - this._metadata = { - // eslint-disable-next-line deprecation/deprecation - ...transactionContext.metadata, - }; - - this._trimEnd = transactionContext.trimEnd; - - this._attributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', - ...this._attributes, - }; - - // this is because transactions are also spans, and spans have a transaction pointer - // TODO (v8): Replace this with another way to set the root span - // eslint-disable-next-line deprecation/deprecation - this.transaction = this; - - // If Dynamic Sampling Context is provided during the creation of the transaction, we freeze it as it usually means - // there is incoming Dynamic Sampling Context. (Either through an incoming request, a baggage meta-tag, or other means) - const incomingDynamicSamplingContext = this._metadata.dynamicSamplingContext; - if (incomingDynamicSamplingContext) { - // We shallow copy this in case anything writes to the original reference of the passed in `dynamicSamplingContext` - this._frozenDynamicSamplingContext = { ...incomingDynamicSamplingContext }; - } - } - - // This sadly conflicts with the getter/setter ordering :( - - /** - * Get the metadata for this transaction. - * @deprecated Use `spanGetMetadata(transaction)` instead. - */ - public get metadata(): TransactionMetadata { - // We merge attributes in for backwards compatibility - return { - // Legacy metadata - ...this._metadata, - - // From attributes - ...(this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE] && { - sampleRate: this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE] as TransactionMetadata['sampleRate'], - }), - }; - } - - /** - * Update the metadata for this transaction. - * @deprecated Use `spanGetMetadata(transaction)` instead. - */ - public set metadata(metadata: TransactionMetadata) { - this._metadata = metadata; - } - - /* eslint-enable @typescript-eslint/member-ordering */ - - /** @inheritdoc */ - public updateName(name: string): this { - this._name = name; - this.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom'); - return this; - } - - /** - * Set the context of a transaction event. - * @deprecated Use either `.setAttribute()`, or set the context on the scope before creating the transaction. - */ - public setContext(key: string, context: Context | null): void { - if (context === null) { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete this._contexts[key]; - } else { - this._contexts[key] = context; - } - } - - /** - * @inheritDoc - * - * @deprecated Use top-level `setMeasurement()` instead. - */ - public setMeasurement(name: string, value: number, unit: MeasurementUnit = ''): void { - this._measurements[name] = { value, unit }; - } - - /** - * Store metadata on this transaction. - * @deprecated Use attributes or store data on the scope instead. - */ - public setMetadata(newMetadata: Partial): void { - this._metadata = { ...this._metadata, ...newMetadata }; - } - - /** - * @inheritDoc - */ - public end(endTimestamp?: SpanTimeInput): string | undefined { - const timestampInS = spanTimeInputToSeconds(endTimestamp); - const transaction = this._finishTransaction(timestampInS); - if (!transaction) { - return undefined; - } - // eslint-disable-next-line deprecation/deprecation - return this._hub.captureEvent(transaction); - } - - /** - * @inheritDoc - */ - public toContext(): TransactionContext { - // eslint-disable-next-line deprecation/deprecation - const spanContext = super.toContext(); - - return dropUndefinedKeys({ - ...spanContext, - name: this._name, - trimEnd: this._trimEnd, - }); - } - - /** - * @inheritdoc - * - * @experimental - * - * @deprecated Use top-level `getDynamicSamplingContextFromSpan` instead. - */ - public getDynamicSamplingContext(): Readonly> { - return getDynamicSamplingContextFromSpan(this); - } - - /** - * Override the current hub with a new one. - * Used if you want another hub to finish the transaction. - * - * @internal - */ - public setHub(hub: Hub): void { - this._hub = hub; - } - - /** - * Finish the transaction & prepare the event to send to Sentry. - */ - protected _finishTransaction(endTimestamp?: number): TransactionEvent | undefined { - // This transaction is already finished, so we should not flush it again. - if (this._endTime !== undefined) { - return undefined; - } - - if (!this._name) { - DEBUG_BUILD && logger.warn('Transaction has no name, falling back to ``.'); - this._name = ''; - } - - // just sets the end timestamp - super.end(endTimestamp); - - // eslint-disable-next-line deprecation/deprecation - const client = this._hub.getClient(); - - if (this._sampled !== true) { - // At this point if `sampled !== true` we want to discard the transaction. - DEBUG_BUILD && logger.log('[Tracing] Discarding transaction because its trace was not chosen to be sampled.'); - - if (client) { - client.recordDroppedEvent('sample_rate', 'transaction'); - } - - return undefined; - } - - // The transaction span itself should be filtered out - const finishedSpans = getSpanDescendants(this).filter(span => span !== this); - - if (this._trimEnd && finishedSpans.length > 0) { - const endTimes = finishedSpans.map(span => spanToJSON(span).timestamp).filter(Boolean) as number[]; - this._endTime = endTimes.reduce((prev, current) => { - return prev > current ? prev : current; - }); - } - - // We want to filter out any incomplete SpanJSON objects - function isFullFinishedSpan(input: Partial): input is SpanJSON { - return !!input.start_timestamp && !!input.timestamp && !!input.span_id && !!input.trace_id; - } - - const spans = finishedSpans.map(span => spanToJSON(span)).filter(isFullFinishedSpan); - - const { scope: capturedSpanScope, isolationScope: capturedSpanIsolationScope } = getCapturedScopesOnSpan(this); - - // eslint-disable-next-line deprecation/deprecation - const { metadata } = this; - - const source = this._attributes['sentry.source'] as TransactionSource | undefined; - - const transaction: TransactionEvent = { - contexts: { - ...this._contexts, - // We don't want to override trace context - trace: spanToTraceContext(this), - }, - spans, - start_timestamp: this._startTime, - timestamp: this._endTime, - transaction: this._name, - type: 'transaction', - sdkProcessingMetadata: { - ...metadata, - capturedSpanScope, - capturedSpanIsolationScope, - ...dropUndefinedKeys({ - dynamicSamplingContext: getDynamicSamplingContextFromSpan(this), - }), - }, - _metrics_summary: getMetricSummaryJsonForSpan(this), - ...(source && { - transaction_info: { - source, - }, - }), - }; - - const hasMeasurements = Object.keys(this._measurements).length > 0; - - if (hasMeasurements) { - DEBUG_BUILD && - logger.log( - '[Measurements] Adding measurements to transaction', - JSON.stringify(this._measurements, undefined, 2), - ); - transaction.measurements = this._measurements; - } - - DEBUG_BUILD && logger.log(`[Tracing] Finishing ${spanToJSON(this).op} transaction: ${this._name}.`); - return transaction; - } -} diff --git a/packages/core/src/trpc.ts b/packages/core/src/trpc.ts new file mode 100644 index 000000000000..f36722b34594 --- /dev/null +++ b/packages/core/src/trpc.ts @@ -0,0 +1,98 @@ +import { isThenable, normalize } from '@sentry/utils'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + captureException, + setContext, + startSpanManual, +} from '.'; +import { getClient } from './currentScopes'; + +interface SentryTrpcMiddlewareOptions { + /** Whether to include procedure inputs in reported events. Defaults to `false`. */ + attachRpcInput?: boolean; +} + +export interface SentryTrpcMiddlewareArguments { + path?: unknown; + type?: unknown; + next: () => T; + rawInput?: unknown; +} + +const trpcCaptureContext = { mechanism: { handled: false, data: { function: 'trpcMiddleware' } } }; + +/** + * Sentry tRPC middleware that captures errors and creates spans for tRPC procedures. + */ +export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) { + return function (opts: SentryTrpcMiddlewareArguments): T { + const { path, type, next, rawInput } = opts; + const client = getClient(); + const clientOptions = client && client.getOptions(); + + const trpcContext: Record = { + procedure_type: type, + }; + + if (options.attachRpcInput !== undefined ? options.attachRpcInput : clientOptions && clientOptions.sendDefaultPii) { + trpcContext.input = normalize(rawInput); + } + + setContext('trpc', trpcContext); + + function captureIfError(nextResult: unknown): void { + // TODO: Set span status based on what TRPCError was encountered + if ( + typeof nextResult === 'object' && + nextResult !== null && + 'ok' in nextResult && + !nextResult.ok && + 'error' in nextResult + ) { + captureException(nextResult.error, trpcCaptureContext); + } + } + + return startSpanManual( + { + name: `trpc/${path}`, + op: 'rpc.server', + forceTransaction: true, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.rpc.trpc', + }, + }, + span => { + let maybePromiseResult; + try { + maybePromiseResult = next(); + } catch (e) { + captureException(e, trpcCaptureContext); + span.end(); + throw e; + } + + if (isThenable(maybePromiseResult)) { + return maybePromiseResult.then( + nextResult => { + captureIfError(nextResult); + span.end(); + return nextResult; + }, + e => { + captureException(e, trpcCaptureContext); + span.end(); + throw e; + }, + ) as T; + } else { + captureIfError(maybePromiseResult); + span.end(); + return maybePromiseResult; + } + }, + ); + }; +} diff --git a/packages/core/src/utils/isSentryRequestUrl.ts b/packages/core/src/utils/isSentryRequestUrl.ts index acc6b0d68cab..a3f3e08be3e0 100644 --- a/packages/core/src/utils/isSentryRequestUrl.ts +++ b/packages/core/src/utils/isSentryRequestUrl.ts @@ -1,20 +1,13 @@ -import type { Client, DsnComponents, Hub } from '@sentry/types'; +import type { Client, DsnComponents } from '@sentry/types'; /** * Checks whether given url points to Sentry server - * @param url url to verify * - * TODO(v8): Remove Hub fallback type + * @param url url to verify */ -export function isSentryRequestUrl(url: string, hubOrClient: Hub | Client | undefined): boolean { - const client = - hubOrClient && isHub(hubOrClient) - ? // eslint-disable-next-line deprecation/deprecation - hubOrClient.getClient() - : hubOrClient; +export function isSentryRequestUrl(url: string, client: Client | undefined): boolean { const dsn = client && client.getDsn(); const tunnel = client && client.getOptions().tunnel; - return checkDsn(url, dsn) || checkTunnel(url, tunnel); } @@ -33,8 +26,3 @@ function checkDsn(url: string, dsn: DsnComponents | undefined): boolean { function removeTrailingSlash(str: string): string { return str[str.length - 1] === '/' ? str.slice(0, -1) : str; } - -function isHub(hubOrClient: Hub | Client | undefined): hubOrClient is Hub { - // eslint-disable-next-line deprecation/deprecation - return (hubOrClient as Hub).getClient !== undefined; -} diff --git a/packages/core/src/utils/parseSampleRate.ts b/packages/core/src/utils/parseSampleRate.ts new file mode 100644 index 000000000000..96bb8c98dec2 --- /dev/null +++ b/packages/core/src/utils/parseSampleRate.ts @@ -0,0 +1,34 @@ +import { logger } from '@sentry/utils'; +import { DEBUG_BUILD } from '../debug-build'; + +/** + * Parse a sample rate from a given value. + * This will either return a boolean or number sample rate, if the sample rate is valid (between 0 and 1). + * If a string is passed, we try to convert it to a number. + * + * Any invalid sample rate will return `undefined`. + */ +export function parseSampleRate(sampleRate: unknown): number | undefined { + if (typeof sampleRate === 'boolean') { + return Number(sampleRate); + } + + const rate = typeof sampleRate === 'string' ? parseFloat(sampleRate) : sampleRate; + if (typeof rate !== 'number' || isNaN(rate)) { + DEBUG_BUILD && + logger.warn( + `[Tracing] Given sample rate is invalid. Sample rate must be a boolean or a number between 0 and 1. Got ${JSON.stringify( + sampleRate, + )} of type ${JSON.stringify(typeof sampleRate)}.`, + ); + return undefined; + } + + if (rate < 0 || rate > 1) { + DEBUG_BUILD && + logger.warn(`[Tracing] Given sample rate is invalid. Sample rate must be between 0 and 1. Got ${rate}.`); + return undefined; + } + + return rate; +} diff --git a/packages/core/test/lib/base.test.ts b/packages/core/test/lib/base.test.ts index 0f789e2da169..373c2d6424cc 100644 --- a/packages/core/test/lib/base.test.ts +++ b/packages/core/test/lib/base.test.ts @@ -1,4 +1,4 @@ -import type { Client, Envelope, Event } from '@sentry/types'; +import type { Client, Envelope, ErrorEvent, Event, TransactionEvent } from '@sentry/types'; import { SentryError, SyncPromise, dsnToString, logger } from '@sentry/utils'; import { Scope, addBreadcrumb, getCurrentScope, getIsolationScope, makeSession, setCurrentClient } from '../../src'; @@ -1065,7 +1065,7 @@ describe('BaseClient', () => { const beforeSend = jest.fn( async event => - new Promise(resolve => { + new Promise(resolve => { setTimeout(() => { resolve(event); }, 1); @@ -1095,7 +1095,7 @@ describe('BaseClient', () => { const beforeSendTransaction = jest.fn( async event => - new Promise(resolve => { + new Promise(resolve => { setTimeout(() => { resolve(event); }, 1); @@ -1125,7 +1125,7 @@ describe('BaseClient', () => { const beforeSend = jest.fn(async event => { event.message = 'changed2'; - return new Promise(resolve => { + return new Promise(resolve => { setTimeout(() => { resolve(event); }, 1); @@ -1155,7 +1155,7 @@ describe('BaseClient', () => { const beforeSendTransaction = jest.fn(async event => { event.transaction = '/adopt/dont/shop'; - return new Promise(resolve => { + return new Promise(resolve => { setTimeout(() => { resolve(event); }, 1); diff --git a/packages/core/test/lib/integration.test.ts b/packages/core/test/lib/integration.test.ts index 46c7eba84e34..d190d5b32dab 100644 --- a/packages/core/test/lib/integration.test.ts +++ b/packages/core/test/lib/integration.test.ts @@ -2,13 +2,7 @@ import type { Integration, Options } from '@sentry/types'; import { logger } from '@sentry/utils'; import { getCurrentScope } from '../../src/currentScopes'; -import { - addIntegration, - convertIntegrationFnToClass, - getIntegrationsToSetup, - installedIntegrations, - setupIntegration, -} from '../../src/integration'; +import { addIntegration, getIntegrationsToSetup, installedIntegrations, setupIntegration } from '../../src/integration'; import { setCurrentClient } from '../../src/sdk'; import { TestClient, getDefaultTestClientOptions } from '../mocks/client'; @@ -699,74 +693,3 @@ describe('addIntegration', () => { expect(logs).toHaveBeenCalledWith('Integration skipped because it was already installed: test'); }); }); - -describe('convertIntegrationFnToClass', () => { - /* eslint-disable deprecation/deprecation */ - it('works with a minimal integration', () => { - const integrationFn = () => ({ - name: 'testName', - setupOnce: () => {}, - }); - - const IntegrationClass = convertIntegrationFnToClass('testName', integrationFn); - - expect(IntegrationClass.id).toBe('testName'); - - const integration = new IntegrationClass(); - expect(integration).toEqual({ - name: 'testName', - setupOnce: expect.any(Function), - }); - }); - - it('works with options', () => { - const integrationFn = (_options: { num: number }) => ({ - name: 'testName', - setupOnce: () => {}, - }); - - const IntegrationClass = convertIntegrationFnToClass('testName', integrationFn); - - expect(IntegrationClass.id).toBe('testName'); - - // not type safe options by default :( - new IntegrationClass(); - - const integration = new IntegrationClass({ num: 3 }); - expect(integration).toEqual({ - name: 'testName', - setupOnce: expect.any(Function), - }); - }); - - it('works with integration hooks', () => { - const setup = jest.fn(); - const setupOnce = jest.fn(); - const processEvent = jest.fn(); - const preprocessEvent = jest.fn(); - - const integrationFn = () => { - return { - name: 'testName', - setup, - setupOnce, - processEvent, - preprocessEvent, - }; - }; - - const IntegrationClass = convertIntegrationFnToClass('testName', integrationFn); - - expect(IntegrationClass.id).toBe('testName'); - - const integration = new IntegrationClass(); - expect(integration).toEqual({ - name: 'testName', - setupOnce, - setup, - processEvent, - preprocessEvent, - }); - }); - /* eslint-enable deprecation/deprecation */ -}); diff --git a/packages/core/test/lib/scope.test.ts b/packages/core/test/lib/scope.test.ts index 267053e6a9b0..bbd40af742d1 100644 --- a/packages/core/test/lib/scope.test.ts +++ b/packages/core/test/lib/scope.test.ts @@ -1,4 +1,4 @@ -import type { Attachment, Breadcrumb, Client, Event, RequestSessionStatus } from '@sentry/types'; +import type { Breadcrumb, Client, Event, RequestSessionStatus } from '@sentry/types'; import { applyScopeDataToEvent, getCurrentScope, @@ -543,29 +543,6 @@ describe('Scope', () => { }); }); - describe('getAttachments', () => { - /* eslint-disable deprecation/deprecation */ - it('works without any data', async () => { - const scope = new Scope(); - - const actual = scope.getAttachments(); - expect(actual).toEqual([]); - }); - - it('works with attachments', async () => { - const attachment1 = { filename: '1' } as Attachment; - const attachment2 = { filename: '2' } as Attachment; - - const scope = new Scope(); - scope.addAttachment(attachment1); - scope.addAttachment(attachment2); - - const actual = scope.getAttachments(); - expect(actual).toEqual([attachment1, attachment2]); - }); - /* eslint-enable deprecation/deprecation */ - }); - describe('setClient() and getClient()', () => { it('allows storing and retrieving client objects', () => { const fakeClient = {} as Client; diff --git a/packages/core/test/lib/tracing/dynamicSamplingContext.test.ts b/packages/core/test/lib/tracing/dynamicSamplingContext.test.ts index f38d3f3e26de..329d13c769af 100644 --- a/packages/core/test/lib/tracing/dynamicSamplingContext.test.ts +++ b/packages/core/test/lib/tracing/dynamicSamplingContext.test.ts @@ -5,11 +5,12 @@ import { setCurrentClient, } from '../../../src'; import { - Transaction, + SentrySpan, addTracingExtensions, getDynamicSamplingContextFromSpan, startInactiveSpan, } from '../../../src/tracing'; +import { freezeDscOnSpan } from '../../../src/tracing/dynamicSamplingContext'; import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; describe('getDynamicSamplingContextFromSpan', () => { @@ -25,26 +26,27 @@ describe('getDynamicSamplingContextFromSpan', () => { jest.resetAllMocks(); }); - test('returns the DSC provided during transaction creation', () => { - // eslint-disable-next-line deprecation/deprecation -- using old API on purpose - const transaction = new Transaction({ + test('uses frozen DSC from span', () => { + const rootSpan = new SentrySpan({ name: 'tx', - metadata: { dynamicSamplingContext: { environment: 'myEnv' } }, + sampled: true, }); - const dynamicSamplingContext = getDynamicSamplingContextFromSpan(transaction); + freezeDscOnSpan(rootSpan, { environment: 'myEnv' }); + + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(rootSpan); expect(dynamicSamplingContext).toStrictEqual({ environment: 'myEnv' }); }); - test('returns a new DSC, if no DSC was provided during transaction creation (via attributes)', () => { - const transaction = startInactiveSpan({ name: 'tx' }); + test('returns a new DSC, if no DSC was provided during rootSpan creation (via attributes)', () => { + const rootSpan = startInactiveSpan({ name: 'tx' }); // Setting the attribute should overwrite the computed values - transaction?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, 0.56); - transaction?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, 0.56); + rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); - const dynamicSamplingContext = getDynamicSamplingContextFromSpan(transaction!); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(rootSpan); expect(dynamicSamplingContext).toStrictEqual({ release: '1.0.1', @@ -56,12 +58,12 @@ describe('getDynamicSamplingContextFromSpan', () => { }); }); - test('returns a new DSC, if no DSC was provided during transaction creation (via deprecated metadata)', () => { - const transaction = startInactiveSpan({ + test('returns a new DSC, if no DSC was provided during rootSpan creation (via deprecated metadata)', () => { + const rootSpan = startInactiveSpan({ name: 'tx', }); - const dynamicSamplingContext = getDynamicSamplingContextFromSpan(transaction!); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(rootSpan); expect(dynamicSamplingContext).toStrictEqual({ release: '1.0.1', @@ -73,20 +75,17 @@ describe('getDynamicSamplingContextFromSpan', () => { }); }); - test('returns a new DSC, if no DSC was provided during transaction creation (via new Txn and deprecated metadata)', () => { - // eslint-disable-next-line deprecation/deprecation -- using old API on purpose - const transaction = new Transaction({ + test('returns a new DSC, if no DSC was provided during rootSpan creation (via new Txn and deprecated metadata)', () => { + const rootSpan = new SentrySpan({ name: 'tx', - metadata: { - sampleRate: 0.56, - }, attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 0.56, [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', }, sampled: true, }); - const dynamicSamplingContext = getDynamicSamplingContextFromSpan(transaction!); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(rootSpan); expect(dynamicSamplingContext).toStrictEqual({ release: '1.0.1', @@ -98,38 +97,34 @@ describe('getDynamicSamplingContextFromSpan', () => { }); }); - describe('Including transaction name in DSC', () => { - test('is not included if transaction source is url', () => { - // eslint-disable-next-line deprecation/deprecation -- using old API on purpose - const transaction = new Transaction({ + describe('Including rootSpan name in DSC', () => { + test('is not included if rootSpan source is url', () => { + const rootSpan = new SentrySpan({ name: 'tx', - metadata: { - sampleRate: 0.56, - }, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 0.56, }, }); - const dsc = getDynamicSamplingContextFromSpan(transaction); + const dsc = getDynamicSamplingContextFromSpan(rootSpan); expect(dsc.transaction).toBeUndefined(); }); test.each([ - ['is included if transaction source is parameterized route/url', 'route'], - ['is included if transaction source is a custom name', 'custom'], + ['is included if rootSpan source is parameterized route/url', 'route'], + ['is included if rootSpan source is a custom name', 'custom'], ] as const)('%s', (_: string, source: TransactionSource) => { - const transaction = startInactiveSpan({ + const rootSpan = startInactiveSpan({ name: 'tx', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, }, }); - // Only setting the attribute manually because we're directly calling new Transaction() - transaction?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); + rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); - const dsc = getDynamicSamplingContextFromSpan(transaction!); + const dsc = getDynamicSamplingContextFromSpan(rootSpan); expect(dsc.transaction).toEqual('tx'); }); diff --git a/packages/core/test/lib/tracing/sentrySpan.test.ts b/packages/core/test/lib/tracing/sentrySpan.test.ts index e7a971d0bdcf..6ed12488000c 100644 --- a/packages/core/test/lib/tracing/sentrySpan.test.ts +++ b/packages/core/test/lib/tracing/sentrySpan.test.ts @@ -1,13 +1,7 @@ import { timestampInSeconds } from '@sentry/utils'; import { SentrySpan } from '../../../src/tracing/sentrySpan'; import { SPAN_STATUS_ERROR } from '../../../src/tracing/spanstatus'; -import { - TRACE_FLAG_NONE, - TRACE_FLAG_SAMPLED, - spanIsSampled, - spanToJSON, - spanToTraceContext, -} from '../../../src/utils/spanUtils'; +import { TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED, spanToJSON, spanToTraceContext } from '../../../src/utils/spanUtils'; describe('SentrySpan', () => { describe('name', () => { @@ -26,17 +20,6 @@ describe('SentrySpan', () => { }); }); - describe('new SentrySpan', () => { - test('simple', () => { - const span = new SentrySpan({ sampled: true }); - // eslint-disable-next-line deprecation/deprecation - const span2 = span.startChild(); - expect(spanToJSON(span2).parent_span_id).toBe(span.spanContext().spanId); - expect(span.spanContext().traceId).toBe(span.spanContext().traceId); - expect(spanIsSampled(span2)).toBe(spanIsSampled(span)); - }); - }); - describe('setters', () => { test('setName', () => { const span = new SentrySpan({}); @@ -351,68 +334,4 @@ describe('SentrySpan', () => { }); }); }); - - // Ensure that attributes & data are merged together - describe('_getData', () => { - it('works without data & attributes', () => { - const span = new SentrySpan(); - - expect(span['_getData']()).toEqual({ - // origin is set by default to 'manual' in the SentrySpan constructor - 'sentry.origin': 'manual', - }); - }); - - it('works with data only', () => { - const span = new SentrySpan(); - // eslint-disable-next-line deprecation/deprecation - span.setData('foo', 'bar'); - - expect(span['_getData']()).toEqual({ - foo: 'bar', - // origin is set by default to 'manual' in the SentrySpan constructor - 'sentry.origin': 'manual', - }); - expect(span['_getData']()).toStrictEqual({ - // eslint-disable-next-line deprecation/deprecation - ...span.data, - 'sentry.origin': 'manual', - }); - }); - - it('works with attributes only', () => { - const span = new SentrySpan(); - span.setAttribute('foo', 'bar'); - - expect(span['_getData']()).toEqual({ - foo: 'bar', - // origin is set by default to 'manual' in the SentrySpan constructor - 'sentry.origin': 'manual', - }); - // eslint-disable-next-line deprecation/deprecation - expect(span['_getData']()).toBe(span.attributes); - }); - - it('merges data & attributes', () => { - const span = new SentrySpan(); - span.setAttribute('foo', 'foo'); - span.setAttribute('bar', 'bar'); - // eslint-disable-next-line deprecation/deprecation - span.setData('foo', 'foo2'); - // eslint-disable-next-line deprecation/deprecation - span.setData('baz', 'baz'); - - expect(span['_getData']()).toEqual({ - foo: 'foo', - bar: 'bar', - baz: 'baz', - // origin is set by default to 'manual' in the SentrySpan constructor - 'sentry.origin': 'manual', - }); - // eslint-disable-next-line deprecation/deprecation - expect(span['_getData']()).not.toBe(span.attributes); - // eslint-disable-next-line deprecation/deprecation - expect(span['_getData']()).not.toBe(span.data); - }); - }); }); diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index 58fc5623f587..02144174ef1f 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -994,7 +994,7 @@ describe('startInactiveSpan', () => { }); }); - it('includes the scope at the time the span was started when finished', async () => { + it('includes the scope at the time the span was started when finished xxx', async () => { const beforeSendTransaction = jest.fn(event => event); const client = new TestClient( diff --git a/packages/core/test/lib/tracing/transaction.test.ts b/packages/core/test/lib/tracing/transaction.test.ts deleted file mode 100644 index aebdc7933fed..000000000000 --- a/packages/core/test/lib/tracing/transaction.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, - SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, - Transaction, - spanToJSON, -} from '../../../src'; - -describe('transaction', () => { - describe('name', () => { - /* eslint-disable deprecation/deprecation */ - it('works with name', () => { - const transaction = new Transaction({ name: 'span name' }); - expect(spanToJSON(transaction).description).toEqual('span name'); - }); - - it('allows to update the name via updateName', () => { - const transaction = new Transaction({ name: 'span name' }); - transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); - expect(spanToJSON(transaction).description).toEqual('span name'); - expect(spanToJSON(transaction).data?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]).toEqual('route'); - - transaction.updateName('new name'); - - expect(spanToJSON(transaction).description).toEqual('new name'); - expect(spanToJSON(transaction).data?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]).toEqual('custom'); - }); - /* eslint-enable deprecation/deprecation */ - }); - - describe('metadata', () => { - /* eslint-disable deprecation/deprecation */ - it('works with defaults', () => { - const transaction = new Transaction({ name: 'span name' }); - expect(transaction.metadata).toEqual({}); - }); - - it('allows to set metadata in constructor', () => { - const transaction = new Transaction({ name: 'span name', metadata: { request: {} } }); - expect(transaction.metadata).toEqual({ - request: {}, - }); - }); - - it('allows to set source & sample rate data in constructor', () => { - const transaction = new Transaction({ - name: 'span name', - metadata: { request: {} }, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 0.5, - }, - }); - - expect(transaction.metadata).toEqual({ - sampleRate: 0.5, - request: {}, - }); - - expect(transaction.attributes).toEqual({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 0.5, - }); - }); - - it('allows to update metadata via setMetadata', () => { - const transaction = new Transaction({ name: 'span name', metadata: {} }); - - transaction.setMetadata({ request: {} }); - - expect(transaction.metadata).toEqual({ - request: {}, - }); - }); - - /* eslint-enable deprecation/deprecation */ - }); -}); diff --git a/packages/core/test/lib/utils/isSentryRequestUrl.test.ts b/packages/core/test/lib/utils/isSentryRequestUrl.test.ts index 98fd7e54207b..b223f856b95e 100644 --- a/packages/core/test/lib/utils/isSentryRequestUrl.test.ts +++ b/packages/core/test/lib/utils/isSentryRequestUrl.test.ts @@ -1,4 +1,4 @@ -import type { Client, Hub } from '@sentry/types'; +import type { Client } from '@sentry/types'; import { isSentryRequestUrl } from '../../../src'; @@ -17,15 +17,6 @@ describe('isSentryRequestUrl', () => { getDsn: () => ({ host: dsn }), } as unknown as Client; - const hub = { - getClient: () => { - return client; - }, - } as unknown as Hub; - - // Works with hub passed - expect(isSentryRequestUrl(url, hub)).toBe(expected); - // Works with client passed expect(isSentryRequestUrl(url, client)).toBe(expected); }); diff --git a/packages/core/test/lib/utils/parseSampleRate.test.ts b/packages/core/test/lib/utils/parseSampleRate.test.ts new file mode 100644 index 000000000000..9c0d91b1810e --- /dev/null +++ b/packages/core/test/lib/utils/parseSampleRate.test.ts @@ -0,0 +1,23 @@ +import { parseSampleRate } from '../../../src/utils/parseSampleRate'; + +describe('parseSampleRate', () => { + it.each([ + [undefined, undefined], + [null, undefined], + [0, 0], + [1, 1], + [0.555, 0.555], + [2, undefined], + [false, 0], + [true, 1], + ['', undefined], + ['aha', undefined], + ['1', 1], + ['1.5', undefined], + ['0.555', 0.555], + ['0', 0], + ])('works with %p', (input, sampleRate) => { + const actual = parseSampleRate(input); + expect(actual).toBe(sampleRate); + }); +}); diff --git a/packages/core/test/lib/utils/spanUtils.test.ts b/packages/core/test/lib/utils/spanUtils.test.ts index b802c7bfb69f..9020a6e59229 100644 --- a/packages/core/test/lib/utils/spanUtils.test.ts +++ b/packages/core/test/lib/utils/spanUtils.test.ts @@ -7,7 +7,6 @@ import { SPAN_STATUS_OK, SPAN_STATUS_UNSET, SentrySpan, - Transaction, addTracingExtensions, setCurrentClient, spanToTraceHeader, @@ -234,7 +233,7 @@ describe('getRootSpan', () => { expect(getRootSpan(root)).toBe(root); }); - it('returns the root span of a child span xxx', () => { + it('returns the root span of a child span', () => { startSpan({ name: 'outer' }, root => { startSpan({ name: 'inner' }, inner => { expect(getRootSpan(inner)).toBe(root); @@ -247,15 +246,4 @@ describe('getRootSpan', () => { }); }); }); - - it('returns the root span of a legacy transaction & its children', () => { - // eslint-disable-next-line deprecation/deprecation - const root = new Transaction({ name: 'test' }); - - expect(getRootSpan(root)).toBe(root); - - // eslint-disable-next-line deprecation/deprecation - const childSpan = root.startChild({ name: 'child' }); - expect(getRootSpan(childSpan)).toBe(root); - }); }); diff --git a/packages/deno/rollup.test.config.mjs b/packages/deno/rollup.test.config.mjs index 8b3393ae2dd4..2902fcfe39b0 100644 --- a/packages/deno/rollup.test.config.mjs +++ b/packages/deno/rollup.test.config.mjs @@ -10,7 +10,10 @@ export default [ output: { file: 'build-test/index.js', sourcemap: true, - preserveModules: false, + preserveModules: + process.env.SENTRY_BUILD_PRESERVE_MODULES === undefined + ? false + : Boolean(process.env.SENTRY_BUILD_PRESERVE_MODULES), strict: false, freeze: false, interop: 'auto', diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index 7f64e0e72132..a78a2392a429 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -13,7 +13,6 @@ export type { StackFrame, Stacktrace, Thread, - Transaction, User, } from '@sentry/types'; export type { AddRequestDataToEventOptions } from '@sentry/utils'; diff --git a/packages/deno/test/normalize.ts b/packages/deno/test/normalize.ts index 9f4481e5fe4a..93f4250c00a8 100644 --- a/packages/deno/test/normalize.ts +++ b/packages/deno/test/normalize.ts @@ -2,7 +2,7 @@ import type { sentryTypes } from '../build-test/index.js'; import { sentryUtils } from '../build-test/index.js'; -type EventOrSession = sentryTypes.Event | sentryTypes.Transaction | sentryTypes.Session; +type EventOrSession = sentryTypes.Event | sentryTypes.Session; export function getNormalizedEvent(envelope: sentryTypes.Envelope): sentryTypes.Event | undefined { let event: sentryTypes.Event | undefined; diff --git a/packages/eslint-config-sdk/src/base.js b/packages/eslint-config-sdk/src/base.js index aa179260dc2c..9a6fa807e09f 100644 --- a/packages/eslint-config-sdk/src/base.js +++ b/packages/eslint-config-sdk/src/base.js @@ -111,22 +111,22 @@ module.exports = { { name: 'window', message: - 'Some global variables are not available in environments like WebWorker or Node.js. Use getGlobalObject() instead.', + 'Some global variables are not available in environments like WebWorker or Node.js. Use GLOBAL_OBJ instead.', }, { name: 'document', message: - 'Some global variables are not available in environments like WebWorker or Node.js. Use getGlobalObject() instead.', + 'Some global variables are not available in environments like WebWorker or Node.js. Use GLOBAL_OBJ instead.', }, { name: 'location', message: - 'Some global variables are not available in environments like WebWorker or Node.js. Use getGlobalObject() instead.', + 'Some global variables are not available in environments like WebWorker or Node.js. Use GLOBAL_OBJ instead.', }, { name: 'navigator', message: - 'Some global variables are not available in environments like WebWorker or Node.js. Use getGlobalObject() instead.', + 'Some global variables are not available in environments like WebWorker or Node.js. Use GLOBAL_OBJ instead.', }, ], diff --git a/packages/eslint-plugin-sdk/package.json b/packages/eslint-plugin-sdk/package.json index a888e9564f7a..ab3c37a6ce30 100644 --- a/packages/eslint-plugin-sdk/package.json +++ b/packages/eslint-plugin-sdk/package.json @@ -21,17 +21,13 @@ "publishConfig": { "access": "public" }, - "dependencies": { - "requireindex": "~1.1.0" - }, - "devDependencies": { - "mocha": "^6.2.0" - }, + "dependencies": {}, "scripts": { "clean": "yarn rimraf sentry-internal-eslint-plugin-sdk-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", - "test": "mocha test --recursive", + "test": "vitest run", + "test:watch": "vitest --watch", "build:tarball": "npm pack", "circularDepCheck": "madge --circular src/index.js" }, diff --git a/packages/eslint-plugin-sdk/test/lib/rules/no-eq-empty.js b/packages/eslint-plugin-sdk/test/lib/rules/no-eq-empty.js deleted file mode 100644 index d9fcbe1e396e..000000000000 --- a/packages/eslint-plugin-sdk/test/lib/rules/no-eq-empty.js +++ /dev/null @@ -1,100 +0,0 @@ -/** - * @fileoverview Rule to disallow using the equality operator with empty array - * @author Abhijeet Prasad - */ -'use strict'; - -// ------------------------------------------------------------------------------ -// Requirements -// ------------------------------------------------------------------------------ - -const RuleTester = require('eslint').RuleTester; - -const rule = require('../../../src/rules/no-eq-empty'); - -// ------------------------------------------------------------------------------ -// Tests -// ------------------------------------------------------------------------------ - -RuleTester.setDefaultConfig({ - parserOptions: { - ecmaVersion: 8, - }, -}); -const ruleTester = new RuleTester(); - -const arrayMessage = 'Do not apply the equality operator on an empty array. Use .length or Array.isArray instead.'; -const objectMessage = 'Do not apply the equality operator on an empty object.'; - -ruleTester.run('no-eq-empty', rule, { - valid: [ - { - code: 'const hey = [] === []', - }, - { - code: 'const hey = [] == []', - }, - { - code: 'const hey = {} === {}', - }, - { - code: 'const hey = {} == {}', - }, - ], - invalid: [ - { - code: 'empty === []', - errors: [ - { - message: arrayMessage, - type: 'BinaryExpression', - }, - ], - }, - { - code: 'empty == []', - errors: [ - { - message: arrayMessage, - type: 'BinaryExpression', - }, - ], - }, - { - code: 'const hey = function() {}() === []', - errors: [ - { - message: arrayMessage, - type: 'BinaryExpression', - }, - ], - }, - { - code: 'empty === {}', - errors: [ - { - message: objectMessage, - type: 'BinaryExpression', - }, - ], - }, - { - code: 'empty == {}', - errors: [ - { - message: objectMessage, - type: 'BinaryExpression', - }, - ], - }, - { - code: 'const hey = function(){}() === {}', - errors: [ - { - message: objectMessage, - type: 'BinaryExpression', - }, - ], - }, - ], -}); diff --git a/packages/eslint-plugin-sdk/test/lib/rules/no-eq-empty.test.ts b/packages/eslint-plugin-sdk/test/lib/rules/no-eq-empty.test.ts new file mode 100644 index 000000000000..9a4c4c5a2ed6 --- /dev/null +++ b/packages/eslint-plugin-sdk/test/lib/rules/no-eq-empty.test.ts @@ -0,0 +1,91 @@ +import { RuleTester } from 'eslint'; +import { describe } from 'vitest'; + +// @ts-expect-error untyped module +import rule from '../../../src/rules/no-eq-empty'; + +describe('no-eq-empty', () => { + test('ruleTester', () => { + const arrayMessage = 'Do not apply the equality operator on an empty array. Use .length or Array.isArray instead.'; + const objectMessage = 'Do not apply the equality operator on an empty object.'; + + const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 8, + }, + }); + + ruleTester.run('no-eq-empty', rule, { + valid: [ + { + code: 'const hey = [] === []', + }, + { + code: 'const hey = [] == []', + }, + { + code: 'const hey = {} === {}', + }, + { + code: 'const hey = {} == {}', + }, + ], + invalid: [ + { + code: 'empty === []', + errors: [ + { + message: arrayMessage, + type: 'BinaryExpression', + }, + ], + }, + { + code: 'empty == []', + errors: [ + { + message: arrayMessage, + type: 'BinaryExpression', + }, + ], + }, + { + code: 'const hey = function() {}() === []', + errors: [ + { + message: arrayMessage, + type: 'BinaryExpression', + }, + ], + }, + { + code: 'empty === {}', + errors: [ + { + message: objectMessage, + type: 'BinaryExpression', + }, + ], + }, + { + code: 'empty == {}', + errors: [ + { + message: objectMessage, + type: 'BinaryExpression', + }, + ], + }, + { + code: 'const hey = function(){}() === {}', + errors: [ + { + message: objectMessage, + type: 'BinaryExpression', + }, + ], + }, + ], + }); + }); +}); diff --git a/packages/tracing-internal/tsconfig.test.json b/packages/eslint-plugin-sdk/tsconfig.test.json similarity index 60% rename from packages/tracing-internal/tsconfig.test.json rename to packages/eslint-plugin-sdk/tsconfig.test.json index 87f6afa06b86..2623e2f6c690 100644 --- a/packages/tracing-internal/tsconfig.test.json +++ b/packages/eslint-plugin-sdk/tsconfig.test.json @@ -1,11 +1,12 @@ { - "extends": "./tsconfig.json", + "extends": "../../tsconfig.json", - "include": ["test/**/*"], + "include": ["test/**/*", "vite.config.ts"], "compilerOptions": { + "allowJs": true, // should include all types from `./tsconfig.json` plus types for all test frameworks used - "types": ["node", "jest"] + "types": [] // other package-specific, test-specific options } diff --git a/packages/eslint-plugin-sdk/vite.config.ts b/packages/eslint-plugin-sdk/vite.config.ts new file mode 100644 index 000000000000..0582a58f479a --- /dev/null +++ b/packages/eslint-plugin-sdk/vite.config.ts @@ -0,0 +1,5 @@ +import baseConfig from '../../vite/vite.config'; + +export default { + ...baseConfig, +}; diff --git a/packages/feedback/rollup.npm.config.mjs b/packages/feedback/rollup.npm.config.mjs index 3dc031c5ff82..cdc5fcf7ad1c 100644 --- a/packages/feedback/rollup.npm.config.mjs +++ b/packages/feedback/rollup.npm.config.mjs @@ -9,7 +9,10 @@ export default makeNPMConfigVariants( exports: 'named', // set preserveModules to false because for feedback we actually want // to bundle everything into one file. - preserveModules: false, + preserveModules: + process.env.SENTRY_BUILD_PRESERVE_MODULES === undefined + ? false + : Boolean(process.env.SENTRY_BUILD_PRESERVE_MODULES), }, }, sucrase: { diff --git a/packages/feedback/src/core/components/Actor.css.ts b/packages/feedback/src/core/components/Actor.css.ts index 4e7a9466cd1e..38c542d6e1b7 100644 --- a/packages/feedback/src/core/components/Actor.css.ts +++ b/packages/feedback/src/core/components/Actor.css.ts @@ -25,6 +25,7 @@ export function createActorStyles(): HTMLStyleElement { cursor: pointer; font-size: 14px; font-weight: 600; + font-family: inherit; padding: 12px 16px; text-decoration: none; z-index: 9000; diff --git a/packages/feedback/src/core/getFeedback.test.ts b/packages/feedback/src/core/getFeedback.test.ts new file mode 100644 index 000000000000..f0cc7b70d0bc --- /dev/null +++ b/packages/feedback/src/core/getFeedback.test.ts @@ -0,0 +1,40 @@ +import { getCurrentScope } from '@sentry/core'; +import { getFeedback } from './getFeedback'; +import { feedbackIntegration } from './integration'; +import { mockSdk } from './mockSdk'; + +describe('getFeedback', () => { + beforeEach(() => { + getCurrentScope().setClient(undefined); + }); + + it('works without a client', () => { + const actual = getFeedback(); + expect(actual).toBeUndefined(); + }); + + it('works with a client without Feedback', () => { + mockSdk({ + sentryOptions: { + integrations: [], + }, + }); + + const actual = getFeedback(); + expect(actual).toBeUndefined(); + }); + + it('works with a client with Feedback', () => { + const feedback = feedbackIntegration(); + + mockSdk({ + sentryOptions: { + integrations: [feedback], + }, + }); + + const actual = getFeedback(); + expect(actual).toBeDefined(); + expect(actual === feedback).toBe(true); + }); +}); diff --git a/packages/feedback/src/core/getFeedback.ts b/packages/feedback/src/core/getFeedback.ts new file mode 100644 index 000000000000..f729d308971c --- /dev/null +++ b/packages/feedback/src/core/getFeedback.ts @@ -0,0 +1,10 @@ +import { getClient } from '@sentry/core'; +import type { feedbackIntegration } from './integration'; + +/** + * This is a small utility to get a type-safe instance of the Feedback integration. + */ +export function getFeedback(): ReturnType | undefined { + const client = getClient(); + return client && client.getIntegrationByName>('Feedback'); +} diff --git a/packages/feedback/src/core/integration.ts b/packages/feedback/src/core/integration.ts index a37aec267730..e0a0e273e6f0 100644 --- a/packages/feedback/src/core/integration.ts +++ b/packages/feedback/src/core/integration.ts @@ -1,4 +1,4 @@ -import { defineIntegration, getClient } from '@sentry/core'; +import { getClient } from '@sentry/core'; import type { Integration, IntegrationFn } from '@sentry/types'; import { isBrowser, logger } from '@sentry/utils'; import { @@ -45,7 +45,10 @@ interface PublicFeedbackIntegration { } export type IFeedbackIntegration = Integration & PublicFeedbackIntegration; -export const _feedbackIntegration = (({ +/** + * Allow users to capture user feedback and send it to Sentry. + */ +export const feedbackIntegration = (({ // FeedbackGeneralConfiguration id = 'sentry-feedback', showBranding = true, @@ -279,5 +282,3 @@ export const _feedbackIntegration = (({ }, }; }) satisfies IntegrationFn; - -export const feedbackIntegration = defineIntegration(_feedbackIntegration); diff --git a/packages/feedback/src/index.ts b/packages/feedback/src/index.ts index e8233ec2455e..c182c98669c3 100644 --- a/packages/feedback/src/index.ts +++ b/packages/feedback/src/index.ts @@ -1,6 +1,7 @@ export { sendFeedback } from './core/sendFeedback'; export { feedbackIntegration } from './core/integration'; export { feedbackModalIntegration } from './modal/integration'; +export { getFeedback } from './core/getFeedback'; export { feedbackScreenshotIntegration } from './screenshot/integration'; export type { OptionalFeedbackConfiguration } from './types'; diff --git a/packages/feedback/src/modal/components/Dialog.css.ts b/packages/feedback/src/modal/components/Dialog.css.ts index 4d5fe338871b..72fe0bd59cb4 100644 --- a/packages/feedback/src/modal/components/Dialog.css.ts +++ b/packages/feedback/src/modal/components/Dialog.css.ts @@ -156,6 +156,7 @@ export function createDialogStyles(): HTMLStyleElement { font-size: 14px; font-weight: 600; padding: 6px 16px; + font-family: inherit; } .btn[disabled] { opacity: 0.6; diff --git a/packages/google-cloud-serverless/package.json b/packages/google-cloud-serverless/package.json index f602834a2341..0519c5efae32 100644 --- a/packages/google-cloud-serverless/package.json +++ b/packages/google-cloud-serverless/package.json @@ -13,7 +13,8 @@ "cjs", "esm", "types", - "types-ts3.8" + "types-ts3.8", + "register.mjs" ], "main": "build/cjs/index.js", "types": "build/types/index.d.ts", @@ -24,7 +25,17 @@ "types": "./build/types/index.d.ts", "default": "./build/cjs/index.js" } - } + }, + "./register": { + "import": { + "default": "./build/register.mjs" + } + }, +"./hook": { + "import": { + "default": "./build/hook.mjs" + } +} }, "typesVersions": { "<4.9": { diff --git a/packages/google-cloud-serverless/rollup.npm.config.mjs b/packages/google-cloud-serverless/rollup.npm.config.mjs index 84a06f2fb64a..5e0b74554d96 100644 --- a/packages/google-cloud-serverless/rollup.npm.config.mjs +++ b/packages/google-cloud-serverless/rollup.npm.config.mjs @@ -1,3 +1,3 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; +import { makeBaseNPMConfig, makeNPMConfigVariants, makeOtelLoaders } from '@sentry-internal/rollup-utils'; -export default makeNPMConfigVariants(makeBaseNPMConfig()); +export default [...makeNPMConfigVariants(makeBaseNPMConfig()), ...makeOtelLoaders('./build', 'sentry-node')]; diff --git a/packages/google-cloud-serverless/src/gcpfunction/http.ts b/packages/google-cloud-serverless/src/gcpfunction/http.ts index 6ce957884ea9..4a27bae2ecf9 100644 --- a/packages/google-cloud-serverless/src/gcpfunction/http.ts +++ b/packages/google-cloud-serverless/src/gcpfunction/http.ts @@ -1,7 +1,6 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, - Transaction, handleCallbackErrors, setHttpStatus, } from '@sentry/core'; @@ -59,14 +58,6 @@ function _wrapHttpFunction(fn: HttpFunction, options: Partial): request: req, }); - if (span instanceof Transaction) { - // We also set __sentry_transaction on the response so people can grab the transaction there to add - // spans to it later. - // TODO(v8): Remove this - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access - (res as any).__sentry_transaction = span; - } - // eslint-disable-next-line @typescript-eslint/unbound-method const _end = res.end; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 8bde94373cef..d07dab055ae2 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -76,6 +76,8 @@ export { expressIntegration, expressErrorHandler, setupExpressErrorHandler, + koaIntegration, + setupKoaErrorHandler, fastifyIntegration, graphqlIntegration, mongoIntegration, @@ -83,6 +85,7 @@ export { mysqlIntegration, mysql2Integration, nestIntegration, + setupNestErrorHandler, postgresIntegration, prismaIntegration, hapiIntegration, @@ -90,6 +93,7 @@ export { spotlightIntegration, initOpenTelemetry, spanToJSON, + trpcMiddleware, } from '@sentry/node'; export { diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 213958d1fd84..162d45e2cda2 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -11,17 +11,47 @@ }, "main": "build/cjs/index.server.js", "module": "build/esm/index.server.js", - "browser": "build/esm/index.client.js", "types": "build/types/index.types.d.ts", "exports": { "./package.json": "./package.json", ".": { + "edge": { + "import": "./build/esm/edge/index.js", + "require": "./build/cjs/edge/index.js", + "default": "./build/esm/edge/index.js" + }, + "edge-light": { + "import": "./build/esm/edge/index.js", + "require": "./build/cjs/edge/index.js", + "default": "./build/esm/edge/index.js" + }, + "worker": { + "import": "./build/esm/edge/index.js", + "require": "./build/cjs/edge/index.js", + "default": "./build/esm/edge/index.js" + }, + "workerd": { + "import": "./build/esm/edge/index.js", + "require": "./build/cjs/edge/index.js", + "default": "./build/esm/edge/index.js" + }, "browser": { "import": "./build/esm/index.client.js", "require": "./build/cjs/index.client.js" }, "node": "./build/cjs/index.server.js", + "import": "./build/esm/index.server.js", "types": "./build/types/index.types.d.ts" + }, + "./register": { + "import": { + "default": "./build/register.mjs" + } + }, + "./hook": { + "import": { + "default": "./build/hook.mjs" + } } }, "typesVersions": { diff --git a/packages/nextjs/rollup.npm.config.mjs b/packages/nextjs/rollup.npm.config.mjs index 39b79c9593b2..afe41659238f 100644 --- a/packages/nextjs/rollup.npm.config.mjs +++ b/packages/nextjs/rollup.npm.config.mjs @@ -1,4 +1,4 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; +import { makeBaseNPMConfig, makeNPMConfigVariants, makeOtelLoaders } from '@sentry-internal/rollup-utils'; export default [ ...makeNPMConfigVariants( @@ -72,4 +72,5 @@ export default [ }, }), ), + ...makeOtelLoaders('./build', 'sentry-node'), ]; diff --git a/packages/nextjs/src/common/types.ts b/packages/nextjs/src/common/types.ts index 1286e8f9ae15..9e74983b078e 100644 --- a/packages/nextjs/src/common/types.ts +++ b/packages/nextjs/src/common/types.ts @@ -1,4 +1,5 @@ -import type { Transaction, WebFetchHeaders, WrappedFunction } from '@sentry/types'; +import type { SentrySpan } from '@sentry/core'; +import type { WebFetchHeaders, WrappedFunction } from '@sentry/types'; import type { NextApiRequest, NextApiResponse } from 'next'; import type { RequestAsyncStorage } from '../config/templates/requestAsyncStorageShim'; @@ -65,7 +66,7 @@ export type AugmentedNextApiRequest = NextApiRequest & { }; export type AugmentedNextApiResponse = NextApiResponse & { - __sentryTransaction?: Transaction; + __sentryTransaction?: SentrySpan; }; export type ResponseEndMethod = AugmentedNextApiResponse['end']; diff --git a/packages/nextjs/src/config/loaders/index.ts b/packages/nextjs/src/config/loaders/index.ts index 27620e004f39..322567c1495b 100644 --- a/packages/nextjs/src/config/loaders/index.ts +++ b/packages/nextjs/src/config/loaders/index.ts @@ -1,4 +1,3 @@ export { default as valueInjectionLoader } from './valueInjectionLoader'; export { default as prefixLoader } from './prefixLoader'; export { default as wrappingLoader } from './wrappingLoader'; -export { default as sdkMultiplexerLoader } from './sdkMultiplexerLoader'; diff --git a/packages/nextjs/src/config/loaders/sdkMultiplexerLoader.ts b/packages/nextjs/src/config/loaders/sdkMultiplexerLoader.ts deleted file mode 100644 index 9373b9d0ced3..000000000000 --- a/packages/nextjs/src/config/loaders/sdkMultiplexerLoader.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { LoaderThis } from './types'; - -type LoaderOptions = { - importTarget: string; -}; - -/** - * This loader allows us to multiplex SDKs depending on what is passed to the `importTarget` loader option. - * If this loader encounters a file that contains the string "__SENTRY_SDK_MULTIPLEXER__" it will replace it's entire - * content with an "export all"-statement that points to `importTarget`. - * - * In our case we use this to multiplex different SDKs depending on whether we're bundling browser code, server code, - * or edge-runtime code. - */ -export default function sdkMultiplexerLoader(this: LoaderThis, userCode: string): string { - if (!userCode.includes('_SENTRY_SDK_MULTIPLEXER')) { - return userCode; - } - - // We know one or the other will be defined, depending on the version of webpack being used - const { importTarget } = 'getOptions' in this ? this.getOptions() : this.query; - - return `export * from "${importTarget}";`; -} diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 824f291a5504..066d26c6d64c 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -24,12 +24,6 @@ import type { } from './types'; import { getWebpackPluginOptions } from './webpackPluginOptions'; -const RUNTIME_TO_SDK_ENTRYPOINT_MAP = { - client: './client', - server: './server', - edge: './edge', -} as const; - // Next.js runs webpack 3 times, once for the client, the server, and for edge. Because we don't want to print certain // warnings 3 times, we keep track of them here. let showedMissingGlobalErrorWarningMsg = false; @@ -79,18 +73,6 @@ export function constructWebpackConfigFunction( // Add a loader which will inject code that sets global values addValueInjectionLoader(newConfig, userNextConfig, userSentryOptions, buildContext); - newConfig.module.rules.push({ - test: /node_modules[/\\]@sentry[/\\]nextjs/, - use: [ - { - loader: path.resolve(__dirname, 'loaders', 'sdkMultiplexerLoader.js'), - options: { - importTarget: RUNTIME_TO_SDK_ENTRYPOINT_MAP[runtime], - }, - }, - ], - }); - let pagesDirPath: string | undefined; const maybePagesDirPath = path.join(projectDir, 'pages'); const maybeSrcPagesDirPath = path.join(projectDir, 'src', 'pages'); diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts index d63aa9ec0256..7c4874d051ad 100644 --- a/packages/nextjs/src/edge/index.ts +++ b/packages/nextjs/src/edge/index.ts @@ -48,7 +48,6 @@ export function withSentryConfig(exportedUserNextConfig: T): T { } export * from '@sentry/vercel-edge'; -export { Transaction } from '@sentry/core'; export * from '../common'; diff --git a/packages/nextjs/src/index.client.ts b/packages/nextjs/src/index.client.ts index c7ae01e45598..4836c98e819b 100644 --- a/packages/nextjs/src/index.client.ts +++ b/packages/nextjs/src/index.client.ts @@ -2,9 +2,3 @@ export * from './client'; // This file is the main entrypoint for non-Next.js build pipelines that use // the package.json's "browser" field or the Edge runtime (Edge API routes and middleware) - -/** - * This const serves no purpose besides being an identifier for this file that the SDK multiplexer loader can use to - * determine that this is in fact a file that wants to be multiplexed. - */ -export const _SENTRY_SDK_MULTIPLEXER = true; diff --git a/packages/nextjs/src/index.server.ts b/packages/nextjs/src/index.server.ts index 4a9536d25ae1..133b6ecf1da0 100644 --- a/packages/nextjs/src/index.server.ts +++ b/packages/nextjs/src/index.server.ts @@ -1,10 +1,2 @@ export * from './config'; export * from './server'; - -// This file is the main entrypoint on the server and/or when the package is `require`d - -/** - * This const serves no purpose besides being an identifier for this file that the SDK multiplexer loader can use to - * determine that this is in fact a file that wants to be multiplexed. - */ -export const _SENTRY_SDK_MULTIPLEXER = true; diff --git a/packages/nextjs/src/index.types.ts b/packages/nextjs/src/index.types.ts index 67d67c66c05c..629560b96312 100644 --- a/packages/nextjs/src/index.types.ts +++ b/packages/nextjs/src/index.types.ts @@ -23,8 +23,6 @@ export declare const getClient: typeof clientSdk.getClient; export declare const getRootSpan: typeof serverSdk.getRootSpan; export declare const continueTrace: typeof clientSdk.continueTrace; -export declare const Integrations: undefined; // TODO(v8): Remove this line. Can only be done when dependencies don't export `Integrations` anymore. - export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; @@ -38,8 +36,6 @@ export declare const createReduxEnhancer: typeof clientSdk.createReduxEnhancer; export declare const showReportDialog: typeof clientSdk.showReportDialog; export declare const withErrorBoundary: typeof clientSdk.withErrorBoundary; -export declare const Transaction: typeof edgeSdk.Transaction; - export declare const metrics: typeof clientSdk.metrics & typeof serverSdk.metrics; export { withSentryConfig } from './config'; diff --git a/packages/nextjs/test/integration/package.json b/packages/nextjs/test/integration/package.json index ea62b01992af..f4c547b5b687 100644 --- a/packages/nextjs/test/integration/package.json +++ b/packages/nextjs/test/integration/package.json @@ -27,12 +27,12 @@ "resolutions": { "@sentry/browser": "file:../../../browser", "@sentry/core": "file:../../../core", - "@sentry/node": "file:../../../node-experimental", + "@sentry/node": "file:../../../node", "@sentry/opentelemetry": "file:../../../opentelemetry", "@sentry/react": "file:../../../react", + "@sentry-internal/browser-utils": "file:../../../browser-utils", "@sentry-internal/replay": "file:../../../replay-internal", "@sentry-internal/replay-canvas": "file:../../../replay-canvas", - "@sentry-internal/tracing": "file:../../../tracing-internal", "@sentry-internal/feedback": "file:../../../feedback", "@sentry/types": "file:../../../types", "@sentry/utils": "file:../../../utils", diff --git a/packages/node-experimental/.eslintrc.js b/packages/node-experimental/.eslintrc.js deleted file mode 100644 index 9d915d4f4c3b..000000000000 --- a/packages/node-experimental/.eslintrc.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - env: { - node: true, - }, - extends: ['../../.eslintrc.js'], - rules: { - '@sentry-internal/sdk/no-optional-chaining': 'off', - '@sentry-internal/sdk/no-nullish-coalescing': 'off', - '@sentry-internal/sdk/no-class-field-initializers': 'off', - }, -}; diff --git a/packages/node-experimental/LICENSE b/packages/node-experimental/LICENSE deleted file mode 100644 index d11896ba1181..000000000000 --- a/packages/node-experimental/LICENSE +++ /dev/null @@ -1,14 +0,0 @@ -Copyright (c) 2023 Sentry (https://sentry.io) and individual contributors. All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the Software without restriction, including without limitation the -rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit -persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the -Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/node-experimental/README.md b/packages/node-experimental/README.md deleted file mode 100644 index 324700199b53..000000000000 --- a/packages/node-experimental/README.md +++ /dev/null @@ -1,59 +0,0 @@ -

    - - Sentry - -

    - -# Official Sentry SDK for Node - -[![npm version](https://img.shields.io/npm/v/@sentry/node.svg)](https://www.npmjs.com/package/@sentry/node) -[![npm dm](https://img.shields.io/npm/dm/@sentry/node.svg)](https://www.npmjs.com/package/@sentry/node) -[![npm dt](https://img.shields.io/npm/dt/@sentry/node.svg)](https://www.npmjs.com/package/@sentry/node) - -## Installation - -```bash -npm install @sentry/node - -# Or yarn -yarn add @sentry/node -``` - -## Usage - -```js -// CJS Syntax -const Sentry = require('@sentry/node'); -// ESM Syntax -import * as Sentry from '@sentry/node'; - -Sentry.init({ - dsn: '__DSN__', - // ... -}); -``` - -Note that it is necessary to initialize Sentry **before you import any package that may be instrumented by us**. - -[More information on how to set up Sentry for Node in v8.](./../../docs/v8-node.md) - -### ESM Support - -Due to the way OpenTelemetry handles instrumentation, this only works out of the box for CommonJS (`require`) -applications. - -There is experimental support for running OpenTelemetry with ESM (`"type": "module"`): - -```bash -node --experimental-loader=@opentelemetry/instrumentation/hook.mjs ./app.js -``` - -You'll need to install `@opentelemetry/instrumentation` in your app to ensure this works. - -See -[OpenTelemetry Instrumentation Docs](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation#instrumentation-for-es-modules-in-nodejs-experimental) -for details on this - but note that this is a) experimental, and b) does not work with all integrations. - -## Links - -- [Official SDK Docs](https://docs.sentry.io/quickstart/) diff --git a/packages/node-experimental/package.json b/packages/node-experimental/package.json deleted file mode 100644 index c23a6ffa7ca9..000000000000 --- a/packages/node-experimental/package.json +++ /dev/null @@ -1,101 +0,0 @@ -{ - "name": "@sentry/node", - "version": "8.0.0-alpha.7", - "description": "Sentry Node SDK using OpenTelemetry for performance instrumentation", - "repository": "git://github.com/getsentry/sentry-javascript.git", - "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/node-experimental", - "author": "Sentry", - "license": "MIT", - "engines": { - "node": ">=14.18" - }, - "files": [ - "cjs", - "esm", - "types", - "types-ts3.8" - ], - "main": "build/cjs/index.js", - "module": "build/esm/index.js", - "types": "build/types/index.d.ts", - "exports": { - "./package.json": "./package.json", - ".": { - "import": { - "types": "./build/types/index.d.ts", - "default": "./build/esm/index.js" - }, - "require": { - "types": "./build/types/index.d.ts", - "default": "./build/cjs/index.js" - } - } - }, - "typesVersions": { - "<4.9": { - "build/types/index.d.ts": [ - "build/types-ts3.8/index.d.ts" - ] - } - }, - "publishConfig": { - "access": "public" - }, - "dependencies": { - "@opentelemetry/api": "1.7.0", - "@opentelemetry/context-async-hooks": "1.21.0", - "@opentelemetry/core": "1.21.0", - "@opentelemetry/instrumentation": "0.48.0", - "@opentelemetry/instrumentation-express": "0.35.0", - "@opentelemetry/instrumentation-fastify": "0.33.0", - "@opentelemetry/instrumentation-graphql": "0.37.0", - "@opentelemetry/instrumentation-hapi": "0.34.0", - "@opentelemetry/instrumentation-http": "0.48.0", - "@opentelemetry/instrumentation-koa": "0.37.0", - "@opentelemetry/instrumentation-mongodb": "0.39.0", - "@opentelemetry/instrumentation-mongoose": "0.35.0", - "@opentelemetry/instrumentation-mysql": "0.35.0", - "@opentelemetry/instrumentation-mysql2": "0.35.0", - "@opentelemetry/instrumentation-nestjs-core": "0.34.0", - "@opentelemetry/instrumentation-pg": "0.38.0", - "@opentelemetry/resources": "1.21.0", - "@opentelemetry/sdk-trace-base": "1.21.0", - "@opentelemetry/semantic-conventions": "1.21.0", - "@prisma/instrumentation": "5.9.0", - "@sentry/core": "8.0.0-alpha.7", - "@sentry/opentelemetry": "8.0.0-alpha.7", - "@sentry/types": "8.0.0-alpha.7", - "@sentry/utils": "8.0.0-alpha.7" - }, - "devDependencies": { - "@types/node": "^14.18.0" - }, - "optionalDependencies": { - "opentelemetry-instrumentation-fetch-node": "1.1.2" - }, - "scripts": { - "build": "run-p build:transpile build:types", - "build:dev": "yarn build", - "build:transpile": "rollup -c rollup.npm.config.mjs", - "build:types": "run-s build:types:core build:types:downlevel", - "build:types:core": "tsc -p tsconfig.types.json", - "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8", - "build:watch": "run-p build:transpile:watch build:types:watch", - "build:dev:watch": "yarn build:watch", - "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:types:watch": "tsc -p tsconfig.types.json --watch", - "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", - "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf build coverage sentry-node-experimental-*.tgz", - "fix": "eslint . --format stylish --fix", - "lint": "eslint . --format stylish", - "test": "yarn test:jest", - "test:jest": "jest", - "test:watch": "jest --watch", - "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push --sig" - }, - "volta": { - "extends": "../../package.json" - }, - "sideEffects": false -} diff --git a/packages/node-experimental/rollup.anr-worker.config.mjs b/packages/node-experimental/rollup.anr-worker.config.mjs deleted file mode 100644 index bd3c1d4b825c..000000000000 --- a/packages/node-experimental/rollup.anr-worker.config.mjs +++ /dev/null @@ -1,31 +0,0 @@ -import { makeBaseBundleConfig } from '@sentry-internal/rollup-utils'; - -export function createAnrWorkerCode() { - let base64Code; - - return { - workerRollupConfig: makeBaseBundleConfig({ - bundleType: 'node-worker', - entrypoints: ['src/integrations/anr/worker.ts'], - licenseTitle: '@sentry/node', - outputFileBase: () => 'worker-script.js', - packageSpecificConfig: { - output: { - dir: 'build/esm/integrations/anr', - sourcemap: false, - }, - plugins: [ - { - name: 'output-base64-worker-script', - renderChunk(code) { - base64Code = Buffer.from(code).toString('base64'); - }, - }, - ], - }, - }), - getBase64Code() { - return base64Code; - }, - }; -} diff --git a/packages/node-experimental/rollup.npm.config.mjs b/packages/node-experimental/rollup.npm.config.mjs deleted file mode 100644 index 17c0727d7eff..000000000000 --- a/packages/node-experimental/rollup.npm.config.mjs +++ /dev/null @@ -1,32 +0,0 @@ -import replace from '@rollup/plugin-replace'; -import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; -import { createAnrWorkerCode } from './rollup.anr-worker.config.mjs'; - -const { workerRollupConfig, getBase64Code } = createAnrWorkerCode(); - -export default [ - // The worker needs to be built first since it's output is used in the main bundle. - workerRollupConfig, - ...makeNPMConfigVariants( - makeBaseNPMConfig({ - packageSpecificConfig: { - output: { - // set exports to 'named' or 'auto' so that rollup doesn't warn - exports: 'named', - // set preserveModules to false because we want to bundle everything into one file. - preserveModules: false, - }, - plugins: [ - replace({ - delimiters: ['###', '###'], - // removes some webpack warnings - preventAssignment: true, - values: { - base64WorkerScript: () => getBase64Code(), - }, - }), - ], - }, - }), - ), -]; diff --git a/packages/node-experimental/src/cron/common.ts b/packages/node-experimental/src/cron/common.ts deleted file mode 100644 index 0fa8c1c18d23..000000000000 --- a/packages/node-experimental/src/cron/common.ts +++ /dev/null @@ -1,51 +0,0 @@ -const replacements: [string, string][] = [ - ['january', '1'], - ['february', '2'], - ['march', '3'], - ['april', '4'], - ['may', '5'], - ['june', '6'], - ['july', '7'], - ['august', '8'], - ['september', '9'], - ['october', '10'], - ['november', '11'], - ['december', '12'], - ['jan', '1'], - ['feb', '2'], - ['mar', '3'], - ['apr', '4'], - ['may', '5'], - ['jun', '6'], - ['jul', '7'], - ['aug', '8'], - ['sep', '9'], - ['oct', '10'], - ['nov', '11'], - ['dec', '12'], - ['sunday', '0'], - ['monday', '1'], - ['tuesday', '2'], - ['wednesday', '3'], - ['thursday', '4'], - ['friday', '5'], - ['saturday', '6'], - ['sun', '0'], - ['mon', '1'], - ['tue', '2'], - ['wed', '3'], - ['thu', '4'], - ['fri', '5'], - ['sat', '6'], -]; - -/** - * Replaces names in cron expressions - */ -export function replaceCronNames(cronExpression: string): string { - return replacements.reduce( - // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor - (acc, [name, replacement]) => acc.replace(new RegExp(name, 'gi'), replacement), - cronExpression, - ); -} diff --git a/packages/node-experimental/src/cron/cron.ts b/packages/node-experimental/src/cron/cron.ts deleted file mode 100644 index 8b6fc324a7a6..000000000000 --- a/packages/node-experimental/src/cron/cron.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { withMonitor } from '@sentry/core'; -import { replaceCronNames } from './common'; - -export type CronJobParams = { - cronTime: string | Date; - onTick: (context: unknown, onComplete?: unknown) => void | Promise; - onComplete?: () => void | Promise; - start?: boolean | null; - context?: unknown; - runOnInit?: boolean | null; - unrefTimeout?: boolean | null; -} & ( - | { - timeZone?: string | null; - utcOffset?: never; - } - | { - timeZone?: never; - utcOffset?: number | null; - } -); - -export type CronJob = { - // -}; - -export type CronJobConstructor = { - from: (param: CronJobParams) => CronJob; - - new ( - cronTime: CronJobParams['cronTime'], - onTick: CronJobParams['onTick'], - onComplete?: CronJobParams['onComplete'], - start?: CronJobParams['start'], - timeZone?: CronJobParams['timeZone'], - context?: CronJobParams['context'], - runOnInit?: CronJobParams['runOnInit'], - utcOffset?: null, - unrefTimeout?: CronJobParams['unrefTimeout'], - ): CronJob; - new ( - cronTime: CronJobParams['cronTime'], - onTick: CronJobParams['onTick'], - onComplete?: CronJobParams['onComplete'], - start?: CronJobParams['start'], - timeZone?: null, - context?: CronJobParams['context'], - runOnInit?: CronJobParams['runOnInit'], - utcOffset?: CronJobParams['utcOffset'], - unrefTimeout?: CronJobParams['unrefTimeout'], - ): CronJob; -}; - -const ERROR_TEXT = 'Automatic instrumentation of CronJob only supports crontab string'; - -/** - * Instruments the `cron` library to send a check-in event to Sentry for each job execution. - * - * ```ts - * import * as Sentry from '@sentry/node'; - * import { CronJob } from 'cron'; - * - * const CronJobWithCheckIn = Sentry.cron.instrumentCron(CronJob, 'my-cron-job'); - * - * // use the constructor - * const job = new CronJobWithCheckIn('* * * * *', () => { - * console.log('You will see this message every minute'); - * }); - * - * // or from - * const job = CronJobWithCheckIn.from({ cronTime: '* * * * *', onTick: () => { - * console.log('You will see this message every minute'); - * }); - * ``` - */ -export function instrumentCron(lib: T & CronJobConstructor, monitorSlug: string): T { - let jobScheduled = false; - - return new Proxy(lib, { - construct(target, args: ConstructorParameters) { - const [cronTime, onTick, onComplete, start, timeZone, ...rest] = args; - - if (typeof cronTime !== 'string') { - throw new Error(ERROR_TEXT); - } - - if (jobScheduled) { - throw new Error(`A job named '${monitorSlug}' has already been scheduled`); - } - - jobScheduled = true; - - const cronString = replaceCronNames(cronTime); - - function monitoredTick(context: unknown, onComplete?: unknown): void | Promise { - return withMonitor( - monitorSlug, - () => { - return onTick(context, onComplete); - }, - { - schedule: { type: 'crontab', value: cronString }, - timezone: timeZone || undefined, - }, - ); - } - - return new target(cronTime, monitoredTick, onComplete, start, timeZone, ...rest); - }, - get(target, prop: keyof CronJobConstructor) { - if (prop === 'from') { - return (param: CronJobParams) => { - const { cronTime, onTick, timeZone } = param; - - if (typeof cronTime !== 'string') { - throw new Error(ERROR_TEXT); - } - - if (jobScheduled) { - throw new Error(`A job named '${monitorSlug}' has already been scheduled`); - } - - jobScheduled = true; - - const cronString = replaceCronNames(cronTime); - - param.onTick = (context: unknown, onComplete?: unknown) => { - return withMonitor( - monitorSlug, - () => { - return onTick(context, onComplete); - }, - { - schedule: { type: 'crontab', value: cronString }, - timezone: timeZone || undefined, - }, - ); - }; - - return target.from(param); - }; - } else { - return target[prop]; - } - }, - }); -} diff --git a/packages/node-experimental/src/cron/node-cron.ts b/packages/node-experimental/src/cron/node-cron.ts deleted file mode 100644 index 4495a0b54909..000000000000 --- a/packages/node-experimental/src/cron/node-cron.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { withMonitor } from '@sentry/core'; -import { replaceCronNames } from './common'; - -export interface NodeCronOptions { - name: string; - timezone?: string; -} - -export interface NodeCron { - schedule: (cronExpression: string, callback: () => void, options: NodeCronOptions) => unknown; -} - -/** - * Wraps the `node-cron` library with check-in monitoring. - * - * ```ts - * import * as Sentry from "@sentry/node"; - * import cron from "node-cron"; - * - * const cronWithCheckIn = Sentry.cron.instrumentNodeCron(cron); - * - * cronWithCheckIn.schedule( - * "* * * * *", - * () => { - * console.log("running a task every minute"); - * }, - * { name: "my-cron-job" }, - * ); - * ``` - */ -export function instrumentNodeCron(lib: Partial & T): T { - return new Proxy(lib, { - get(target, prop: keyof NodeCron) { - if (prop === 'schedule' && target.schedule) { - // When 'get' is called for schedule, return a proxied version of the schedule function - return new Proxy(target.schedule, { - apply(target, thisArg, argArray: Parameters) { - const [expression, , options] = argArray; - - if (!options?.name) { - throw new Error('Missing "name" for scheduled job. A name is required for Sentry check-in monitoring.'); - } - - return withMonitor( - options.name, - () => { - return target.apply(thisArg, argArray); - }, - { - schedule: { type: 'crontab', value: replaceCronNames(expression) }, - timezone: options?.timezone, - }, - ); - }, - }); - } else { - return target[prop]; - } - }, - }); -} diff --git a/packages/node-experimental/src/cron/node-schedule.ts b/packages/node-experimental/src/cron/node-schedule.ts deleted file mode 100644 index 79ae44a06e52..000000000000 --- a/packages/node-experimental/src/cron/node-schedule.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { withMonitor } from '@sentry/core'; -import { replaceCronNames } from './common'; - -export interface NodeSchedule { - scheduleJob( - nameOrExpression: string | Date | object, - expressionOrCallback: string | Date | object | (() => void), - callback?: () => void, - ): unknown; -} - -/** - * Instruments the `node-schedule` library to send a check-in event to Sentry for each job execution. - * - * ```ts - * import * as Sentry from '@sentry/node'; - * import * as schedule from 'node-schedule'; - * - * const scheduleWithCheckIn = Sentry.cron.instrumentNodeSchedule(schedule); - * - * const job = scheduleWithCheckIn.scheduleJob('my-cron-job', '* * * * *', () => { - * console.log('You will see this message every minute'); - * }); - * ``` - */ -export function instrumentNodeSchedule(lib: T & NodeSchedule): T { - return new Proxy(lib, { - get(target, prop: keyof NodeSchedule) { - if (prop === 'scheduleJob') { - // eslint-disable-next-line @typescript-eslint/unbound-method - return new Proxy(target.scheduleJob, { - apply(target, thisArg, argArray: Parameters) { - const [nameOrExpression, expressionOrCallback] = argArray; - - if (typeof nameOrExpression !== 'string' || typeof expressionOrCallback !== 'string') { - throw new Error( - "Automatic instrumentation of 'node-schedule' requires the first parameter of 'scheduleJob' to be a job name string and the second parameter to be a crontab string", - ); - } - - const monitorSlug = nameOrExpression; - const expression = expressionOrCallback; - - return withMonitor( - monitorSlug, - () => { - return target.apply(thisArg, argArray); - }, - { - schedule: { type: 'crontab', value: replaceCronNames(expression) }, - }, - ); - }, - }); - } - - return target[prop]; - }, - }); -} diff --git a/packages/node-experimental/src/index.ts b/packages/node-experimental/src/index.ts deleted file mode 100644 index 33bbcca54213..000000000000 --- a/packages/node-experimental/src/index.ts +++ /dev/null @@ -1,127 +0,0 @@ -export { httpIntegration } from './integrations/http'; -export { nativeNodeFetchIntegration } from './integrations/node-fetch'; - -export { consoleIntegration } from './integrations/console'; -export { nodeContextIntegration } from './integrations/context'; -export { contextLinesIntegration } from './integrations/contextlines'; -export { localVariablesIntegration } from './integrations/local-variables'; -export { modulesIntegration } from './integrations/modules'; -export { onUncaughtExceptionIntegration } from './integrations/onuncaughtexception'; -export { onUnhandledRejectionIntegration } from './integrations/onunhandledrejection'; -export { anrIntegration } from './integrations/anr'; - -export { expressIntegration, expressErrorHandler, setupExpressErrorHandler } from './integrations/tracing/express'; -export { fastifyIntegration, setupFastifyErrorHandler } from './integrations/tracing/fastify'; -export { graphqlIntegration } from './integrations/tracing/graphql'; -export { mongoIntegration } from './integrations/tracing/mongo'; -export { mongooseIntegration } from './integrations/tracing/mongoose'; -export { mysqlIntegration } from './integrations/tracing/mysql'; -export { mysql2Integration } from './integrations/tracing/mysql2'; -export { nestIntegration } from './integrations/tracing/nest'; -export { postgresIntegration } from './integrations/tracing/postgres'; -export { prismaIntegration } from './integrations/tracing/prisma'; -export { hapiIntegration, setupHapiErrorHandler } from './integrations/tracing/hapi'; -export { spotlightIntegration } from './integrations/spotlight'; - -export { init, getDefaultIntegrations } from './sdk/init'; -export { initOpenTelemetry } from './sdk/initOtel'; -export { getAutoPerformanceIntegrations } from './integrations/tracing'; -export { getSentryRelease, defaultStackParser } from './sdk/api'; -export { createGetModuleFromFilename } from './utils/module'; -export { makeNodeTransport } from './transports'; -export { NodeClient } from './sdk/client'; -export { cron } from './cron'; - -export type { NodeOptions } from './types'; - -export { - addRequestDataToEvent, - DEFAULT_USER_INCLUDES, - extractRequestData, -} from '@sentry/utils'; - -// These are custom variants that need to be used instead of the core one -// As they have slightly different implementations -export { continueTrace } from '@sentry/opentelemetry'; - -export { - addBreadcrumb, - isInitialized, - getGlobalScope, - close, - createTransport, - flush, - Hub, - SDK_VERSION, - getSpanStatusFromHttpCode, - setHttpStatus, - captureCheckIn, - withMonitor, - requestDataIntegration, - functionToStringIntegration, - inboundFiltersIntegration, - linkedErrorsIntegration, - addEventProcessor, - setContext, - setExtra, - setExtras, - setTag, - setTags, - setUser, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, - setCurrentClient, - Scope, - setMeasurement, - getSpanDescendants, - parameterize, - getClient, - // eslint-disable-next-line deprecation/deprecation - getCurrentHub, - getCurrentScope, - getIsolationScope, - withScope, - withIsolationScope, - captureException, - captureEvent, - captureMessage, - captureConsoleIntegration, - debugIntegration, - dedupeIntegration, - extraErrorDataIntegration, - rewriteFramesIntegration, - sessionTimingIntegration, - metricsDefault as metrics, - startSession, - captureSession, - endSession, - addIntegration, - startSpan, - startSpanManual, - startInactiveSpan, - getActiveSpan, - withActiveSpan, - getRootSpan, - spanToJSON, -} from '@sentry/core'; - -export type { - Breadcrumb, - BreadcrumbHint, - PolymorphicRequest, - Request, - SdkInfo, - Event, - EventHint, - Exception, - Session, - SeverityLevel, - StackFrame, - Stacktrace, - Thread, - Transaction, - User, - Span, -} from '@sentry/types'; diff --git a/packages/node-experimental/src/integrations/anr/common.ts b/packages/node-experimental/src/integrations/anr/common.ts deleted file mode 100644 index e2e50fae4179..000000000000 --- a/packages/node-experimental/src/integrations/anr/common.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { Contexts, DsnComponents, Primitive, SdkMetadata } from '@sentry/types'; - -export interface AnrIntegrationOptions { - /** - * Interval to send heartbeat messages to the ANR worker. - * - * Defaults to 50ms. - */ - pollInterval: number; - /** - * Threshold in milliseconds to trigger an ANR event. - * - * Defaults to 5000ms. - */ - anrThreshold: number; - /** - * Whether to capture a stack trace when the ANR event is triggered. - * - * Defaults to `false`. - * - * This uses the node debugger which enables the inspector API and opens the required ports. - */ - captureStackTrace: boolean; - /** - * Tags to include with ANR events. - */ - staticTags: { [key: string]: Primitive }; - /** - * @ignore Internal use only. - * - * If this is supplied, stack frame filenames will be rewritten to be relative to this path. - */ - appRootPath: string | undefined; -} - -export interface WorkerStartData extends AnrIntegrationOptions { - debug: boolean; - sdkMetadata: SdkMetadata; - dsn: DsnComponents; - tunnel: string | undefined; - release: string | undefined; - environment: string; - dist: string | undefined; - contexts: Contexts; -} diff --git a/packages/node-experimental/src/integrations/anr/index.ts b/packages/node-experimental/src/integrations/anr/index.ts deleted file mode 100644 index 7dbe9e905cb4..000000000000 --- a/packages/node-experimental/src/integrations/anr/index.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { defineIntegration, mergeScopeData } from '@sentry/core'; -import type { Contexts, Event, EventHint, Integration, IntegrationFn, ScopeData } from '@sentry/types'; -import { GLOBAL_OBJ, logger } from '@sentry/utils'; -import * as inspector from 'inspector'; -import { Worker } from 'worker_threads'; -import { getCurrentScope, getGlobalScope, getIsolationScope } from '../..'; -import { NODE_VERSION } from '../../nodeVersion'; -import type { NodeClient } from '../../sdk/client'; -import type { AnrIntegrationOptions, WorkerStartData } from './common'; -import { base64WorkerScript } from './worker-script'; - -const DEFAULT_INTERVAL = 50; -const DEFAULT_HANG_THRESHOLD = 5000; - -function log(message: string, ...args: unknown[]): void { - logger.log(`[ANR] ${message}`, ...args); -} - -function globalWithScopeFetchFn(): typeof GLOBAL_OBJ & { __SENTRY_GET_SCOPES__?: () => ScopeData } { - return GLOBAL_OBJ; -} - -/** Fetches merged scope data */ -function getScopeData(): ScopeData { - const scope = getGlobalScope().getScopeData(); - mergeScopeData(scope, getIsolationScope().getScopeData()); - mergeScopeData(scope, getCurrentScope().getScopeData()); - - // We remove attachments because they likely won't serialize well as json - scope.attachments = []; - // We can't serialize event processor functions - scope.eventProcessors = []; - - return scope; -} - -/** - * Gets contexts by calling all event processors. This shouldn't be called until all integrations are setup - */ -async function getContexts(client: NodeClient): Promise { - let event: Event | null = { message: 'ANR' }; - const eventHint: EventHint = {}; - - for (const processor of client.getEventProcessors()) { - if (event === null) break; - event = await processor(event, eventHint); - } - - return event?.contexts || {}; -} - -const INTEGRATION_NAME = 'Anr'; - -type AnrInternal = { startWorker: () => void; stopWorker: () => void }; - -const _anrIntegration = ((options: Partial = {}) => { - if (NODE_VERSION.major < 16 || (NODE_VERSION.major === 16 && NODE_VERSION.minor < 17)) { - throw new Error('ANR detection requires Node 16.17.0 or later'); - } - - let worker: Promise<() => void> | undefined; - let client: NodeClient | undefined; - - // Hookup the scope fetch function to the global object so that it can be called from the worker thread via the - // debugger when it pauses - const gbl = globalWithScopeFetchFn(); - gbl.__SENTRY_GET_SCOPES__ = getScopeData; - - return { - name: INTEGRATION_NAME, - startWorker: () => { - if (worker) { - return; - } - - if (client) { - worker = _startWorker(client, options); - } - }, - stopWorker: () => { - if (worker) { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - worker.then(stop => { - stop(); - worker = undefined; - }); - } - }, - setup(initClient: NodeClient) { - client = initClient; - - // setImmediate is used to ensure that all other integrations have had their setup called first. - // This allows us to call into all integrations to fetch the full context - setImmediate(() => this.startWorker()); - }, - } as Integration & AnrInternal; -}) satisfies IntegrationFn; - -type AnrReturn = (options?: Partial) => Integration & AnrInternal; - -export const anrIntegration = defineIntegration(_anrIntegration) as AnrReturn; - -/** - * Starts the ANR worker thread - * - * @returns A function to stop the worker - */ -async function _startWorker( - client: NodeClient, - integrationOptions: Partial, -): Promise<() => void> { - const dsn = client.getDsn(); - - if (!dsn) { - return () => { - // - }; - } - - const contexts = await getContexts(client); - - // These will not be accurate if sent later from the worker thread - delete contexts.app?.app_memory; - delete contexts.device?.free_memory; - - const initOptions = client.getOptions(); - - const sdkMetadata = client.getSdkMetadata() || {}; - if (sdkMetadata.sdk) { - sdkMetadata.sdk.integrations = initOptions.integrations.map(i => i.name); - } - - const options: WorkerStartData = { - debug: logger.isEnabled(), - dsn, - tunnel: initOptions.tunnel, - environment: initOptions.environment || 'production', - release: initOptions.release, - dist: initOptions.dist, - sdkMetadata, - appRootPath: integrationOptions.appRootPath, - pollInterval: integrationOptions.pollInterval || DEFAULT_INTERVAL, - anrThreshold: integrationOptions.anrThreshold || DEFAULT_HANG_THRESHOLD, - captureStackTrace: !!integrationOptions.captureStackTrace, - staticTags: integrationOptions.staticTags || {}, - contexts, - }; - - if (options.captureStackTrace) { - if (!inspector.url()) { - inspector.open(0); - } - } - - const worker = new Worker(new URL(`data:application/javascript;base64,${base64WorkerScript}`), { - workerData: options, - }); - - process.on('exit', () => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - worker.terminate(); - }); - - const timer = setInterval(() => { - try { - const currentSession = getCurrentScope().getSession(); - // We need to copy the session object and remove the toJSON method so it can be sent to the worker - // serialized without making it a SerializedSession - const session = currentSession ? { ...currentSession, toJSON: undefined } : undefined; - // message the worker to tell it the main event loop is still running - worker.postMessage({ session }); - } catch (_) { - // - } - }, options.pollInterval); - // Timer should not block exit - timer.unref(); - - worker.on('message', (msg: string) => { - if (msg === 'session-ended') { - log('ANR event sent from ANR worker. Clearing session in this thread.'); - getCurrentScope().setSession(undefined); - } - }); - - worker.once('error', (err: Error) => { - clearInterval(timer); - log('ANR worker error', err); - }); - - worker.once('exit', (code: number) => { - clearInterval(timer); - log('ANR worker exit', code); - }); - - // Ensure this thread can't block app exit - worker.unref(); - - return () => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - worker.terminate(); - clearInterval(timer); - }; -} diff --git a/packages/node-experimental/src/integrations/anr/worker-script.ts b/packages/node-experimental/src/integrations/anr/worker-script.ts deleted file mode 100644 index c70323e0fc50..000000000000 --- a/packages/node-experimental/src/integrations/anr/worker-script.ts +++ /dev/null @@ -1,2 +0,0 @@ -// This string is a placeholder that gets overwritten with the worker code. -export const base64WorkerScript = '###base64WorkerScript###'; diff --git a/packages/node-experimental/src/integrations/anr/worker.ts b/packages/node-experimental/src/integrations/anr/worker.ts deleted file mode 100644 index 21bdcbbb0631..000000000000 --- a/packages/node-experimental/src/integrations/anr/worker.ts +++ /dev/null @@ -1,271 +0,0 @@ -import { - applyScopeDataToEvent, - createEventEnvelope, - createSessionEnvelope, - getEnvelopeEndpointWithUrlEncodedAuth, - makeSession, - updateSession, -} from '@sentry/core'; -import type { Event, ScopeData, Session, StackFrame } from '@sentry/types'; -import { - callFrameToStackFrame, - normalizeUrlToBase, - stripSentryFramesAndReverse, - uuid4, - watchdogTimer, -} from '@sentry/utils'; -import { Session as InspectorSession } from 'inspector'; -import { parentPort, workerData } from 'worker_threads'; - -import { makeNodeTransport } from '../../transports'; -import { createGetModuleFromFilename } from '../../utils/module'; -import type { WorkerStartData } from './common'; - -type VoidFunction = () => void; - -const options: WorkerStartData = workerData; -let session: Session | undefined; -let hasSentAnrEvent = false; - -function log(msg: string): void { - if (options.debug) { - // eslint-disable-next-line no-console - console.log(`[ANR Worker] ${msg}`); - } -} - -const url = getEnvelopeEndpointWithUrlEncodedAuth(options.dsn, options.tunnel, options.sdkMetadata.sdk); -const transport = makeNodeTransport({ - url, - recordDroppedEvent: () => { - // - }, -}); - -async function sendAbnormalSession(): Promise { - // of we have an existing session passed from the main thread, send it as abnormal - if (session) { - log('Sending abnormal session'); - updateSession(session, { status: 'abnormal', abnormal_mechanism: 'anr_foreground' }); - - const envelope = createSessionEnvelope(session, options.dsn, options.sdkMetadata, options.tunnel); - // Log the envelope so to aid in testing - log(JSON.stringify(envelope)); - - await transport.send(envelope); - - try { - // Notify the main process that the session has ended so the session can be cleared from the scope - parentPort?.postMessage('session-ended'); - } catch (_) { - // ignore - } - } -} - -log('Started'); - -function prepareStackFrames(stackFrames: StackFrame[] | undefined): StackFrame[] | undefined { - if (!stackFrames) { - return undefined; - } - - // Strip Sentry frames and reverse the stack frames so they are in the correct order - const strippedFrames = stripSentryFramesAndReverse(stackFrames); - - // If we have an app root path, rewrite the filenames to be relative to the app root - if (options.appRootPath) { - for (const frame of strippedFrames) { - if (!frame.filename) { - continue; - } - - frame.filename = normalizeUrlToBase(frame.filename, options.appRootPath); - } - } - - return strippedFrames; -} - -function applyScopeToEvent(event: Event, scope: ScopeData): void { - applyScopeDataToEvent(event, scope); - - if (!event.contexts?.trace) { - const { traceId, spanId, parentSpanId } = scope.propagationContext; - event.contexts = { - trace: { - trace_id: traceId, - span_id: spanId, - parent_span_id: parentSpanId, - }, - ...event.contexts, - }; - } -} - -async function sendAnrEvent(frames?: StackFrame[], scope?: ScopeData): Promise { - if (hasSentAnrEvent) { - return; - } - - hasSentAnrEvent = true; - - await sendAbnormalSession(); - - log('Sending event'); - - const event: Event = { - event_id: uuid4(), - contexts: options.contexts, - release: options.release, - environment: options.environment, - dist: options.dist, - platform: 'node', - level: 'error', - exception: { - values: [ - { - type: 'ApplicationNotResponding', - value: `Application Not Responding for at least ${options.anrThreshold} ms`, - stacktrace: { frames: prepareStackFrames(frames) }, - // This ensures the UI doesn't say 'Crashed in' for the stack trace - mechanism: { type: 'ANR' }, - }, - ], - }, - tags: options.staticTags, - }; - - if (scope) { - applyScopeToEvent(event, scope); - } - - const envelope = createEventEnvelope(event, options.dsn, options.sdkMetadata, options.tunnel); - // Log the envelope to aid in testing - log(JSON.stringify(envelope)); - - await transport.send(envelope); - await transport.flush(2000); - - // Delay for 5 seconds so that stdio can flush if the main event loop ever restarts. - // This is mainly for the benefit of logging or debugging. - setTimeout(() => { - process.exit(0); - }, 5_000); -} - -let debuggerPause: VoidFunction | undefined; - -if (options.captureStackTrace) { - log('Connecting to debugger'); - - const session = new InspectorSession(); - session.connectToMainThread(); - - log('Connected to debugger'); - - // Collect scriptId -> url map so we can look up the filenames later - const scripts = new Map(); - - session.on('Debugger.scriptParsed', event => { - scripts.set(event.params.scriptId, event.params.url); - }); - - session.on('Debugger.paused', event => { - if (event.params.reason !== 'other') { - return; - } - - try { - log('Debugger paused'); - - // copy the frames - const callFrames = [...event.params.callFrames]; - - const getModuleName = options.appRootPath ? createGetModuleFromFilename(options.appRootPath) : () => undefined; - const stackFrames = callFrames.map(frame => - callFrameToStackFrame(frame, scripts.get(frame.location.scriptId), getModuleName), - ); - - // Evaluate a script in the currently paused context - session.post( - 'Runtime.evaluate', - { - // Grab the trace context from the current scope - expression: 'global.__SENTRY_GET_SCOPES__();', - // Don't re-trigger the debugger if this causes an error - silent: true, - // Serialize the result to json otherwise only primitives are supported - returnByValue: true, - }, - (err, param) => { - if (err) { - log(`Error executing script: '${err.message}'`); - } - - const scopes = param && param.result ? (param.result.value as ScopeData) : undefined; - - session.post('Debugger.resume'); - session.post('Debugger.disable'); - - sendAnrEvent(stackFrames, scopes).then(null, () => { - log('Sending ANR event failed.'); - }); - }, - ); - } catch (e) { - session.post('Debugger.resume'); - session.post('Debugger.disable'); - throw e; - } - }); - - debuggerPause = () => { - try { - session.post('Debugger.enable', () => { - session.post('Debugger.pause'); - }); - } catch (_) { - // - } - }; -} - -function createHrTimer(): { getTimeMs: () => number; reset: VoidFunction } { - // TODO (v8): We can use process.hrtime.bigint() after we drop node v8 - let lastPoll = process.hrtime(); - - return { - getTimeMs: (): number => { - const [seconds, nanoSeconds] = process.hrtime(lastPoll); - return Math.floor(seconds * 1e3 + nanoSeconds / 1e6); - }, - reset: (): void => { - lastPoll = process.hrtime(); - }, - }; -} - -function watchdogTimeout(): void { - log('Watchdog timeout'); - - if (debuggerPause) { - log('Pausing debugger to capture stack trace'); - debuggerPause(); - } else { - log('Capturing event without a stack trace'); - sendAnrEvent().then(null, () => { - log('Sending ANR event failed on watchdog timeout.'); - }); - } -} - -const { poll } = watchdogTimer(createHrTimer, options.pollInterval, options.anrThreshold, watchdogTimeout); - -parentPort?.on('message', (msg: { session: Session | undefined }) => { - if (msg.session) { - session = makeSession(msg.session); - } - - poll(); -}); diff --git a/packages/node-experimental/src/integrations/console.ts b/packages/node-experimental/src/integrations/console.ts deleted file mode 100644 index 0b3d27fe8510..000000000000 --- a/packages/node-experimental/src/integrations/console.ts +++ /dev/null @@ -1,36 +0,0 @@ -import * as util from 'util'; -import { addBreadcrumb, defineIntegration, getClient } from '@sentry/core'; -import type { IntegrationFn } from '@sentry/types'; -import { addConsoleInstrumentationHandler, severityLevelFromString } from '@sentry/utils'; - -const INTEGRATION_NAME = 'Console'; - -const _consoleIntegration = (() => { - return { - name: INTEGRATION_NAME, - setup(client) { - addConsoleInstrumentationHandler(({ args, level }) => { - if (getClient() !== client) { - return; - } - - addBreadcrumb( - { - category: 'console', - level: severityLevelFromString(level), - message: util.format.apply(undefined, args), - }, - { - input: [...args], - level, - }, - ); - }); - }, - }; -}) satisfies IntegrationFn; - -/** - * Capture console logs as breadcrumbs. - */ -export const consoleIntegration = defineIntegration(_consoleIntegration); diff --git a/packages/node-experimental/src/integrations/context.ts b/packages/node-experimental/src/integrations/context.ts deleted file mode 100644 index c33d97e79044..000000000000 --- a/packages/node-experimental/src/integrations/context.ts +++ /dev/null @@ -1,452 +0,0 @@ -import { execFile } from 'child_process'; -import { readFile, readdir } from 'fs'; -import * as os from 'os'; -import { join } from 'path'; -import { promisify } from 'util'; -import { defineIntegration } from '@sentry/core'; -import type { - AppContext, - CloudResourceContext, - Contexts, - CultureContext, - DeviceContext, - Event, - IntegrationFn, - OsContext, -} from '@sentry/types'; - -export const readFileAsync = promisify(readFile); -export const readDirAsync = promisify(readdir); - -const INTEGRATION_NAME = 'Context'; - -interface DeviceContextOptions { - cpu?: boolean; - memory?: boolean; -} - -interface ContextOptions { - app?: boolean; - os?: boolean; - device?: DeviceContextOptions | boolean; - culture?: boolean; - cloudResource?: boolean; -} - -const _nodeContextIntegration = ((options: ContextOptions = {}) => { - let cachedContext: Promise | undefined; - - const _options = { - app: true, - os: true, - device: true, - culture: true, - cloudResource: true, - ...options, - }; - - /** Add contexts to the event. Caches the context so we only look it up once. */ - async function addContext(event: Event): Promise { - if (cachedContext === undefined) { - cachedContext = _getContexts(); - } - - const updatedContext = _updateContext(await cachedContext); - - event.contexts = { - ...event.contexts, - app: { ...updatedContext.app, ...event.contexts?.app }, - os: { ...updatedContext.os, ...event.contexts?.os }, - device: { ...updatedContext.device, ...event.contexts?.device }, - culture: { ...updatedContext.culture, ...event.contexts?.culture }, - cloud_resource: { ...updatedContext.cloud_resource, ...event.contexts?.cloud_resource }, - }; - - return event; - } - - /** Get the contexts from node. */ - async function _getContexts(): Promise { - const contexts: Contexts = {}; - - if (_options.os) { - contexts.os = await getOsContext(); - } - - if (_options.app) { - contexts.app = getAppContext(); - } - - if (_options.device) { - contexts.device = getDeviceContext(_options.device); - } - - if (_options.culture) { - const culture = getCultureContext(); - - if (culture) { - contexts.culture = culture; - } - } - - if (_options.cloudResource) { - contexts.cloud_resource = getCloudResourceContext(); - } - - return contexts; - } - - return { - name: INTEGRATION_NAME, - processEvent(event) { - return addContext(event); - }, - }; -}) satisfies IntegrationFn; - -/** - * Capture context about the environment and the device that the client is running on, to events. - */ -export const nodeContextIntegration = defineIntegration(_nodeContextIntegration); - -/** - * Updates the context with dynamic values that can change - */ -function _updateContext(contexts: Contexts): Contexts { - // Only update properties if they exist - if (contexts?.app?.app_memory) { - contexts.app.app_memory = process.memoryUsage().rss; - } - - if (contexts?.device?.free_memory) { - contexts.device.free_memory = os.freemem(); - } - - return contexts; -} - -/** - * Returns the operating system context. - * - * Based on the current platform, this uses a different strategy to provide the - * most accurate OS information. Since this might involve spawning subprocesses - * or accessing the file system, this should only be executed lazily and cached. - * - * - On macOS (Darwin), this will execute the `sw_vers` utility. The context - * has a `name`, `version`, `build` and `kernel_version` set. - * - On Linux, this will try to load a distribution release from `/etc` and set - * the `name`, `version` and `kernel_version` fields. - * - On all other platforms, only a `name` and `version` will be returned. Note - * that `version` might actually be the kernel version. - */ -async function getOsContext(): Promise { - const platformId = os.platform(); - switch (platformId) { - case 'darwin': - return getDarwinInfo(); - case 'linux': - return getLinuxInfo(); - default: - return { - name: PLATFORM_NAMES[platformId] || platformId, - version: os.release(), - }; - } -} - -function getCultureContext(): CultureContext | undefined { - try { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any - if (typeof (process.versions as unknown as any).icu !== 'string') { - // Node was built without ICU support - return; - } - - // Check that node was built with full Intl support. Its possible it was built without support for non-English - // locales which will make resolvedOptions inaccurate - // - // https://nodejs.org/api/intl.html#detecting-internationalization-support - const january = new Date(9e8); - const spanish = new Intl.DateTimeFormat('es', { month: 'long' }); - if (spanish.format(january) === 'enero') { - const options = Intl.DateTimeFormat().resolvedOptions(); - - return { - locale: options.locale, - timezone: options.timeZone, - }; - } - } catch (err) { - // - } - - return; -} - -function getAppContext(): AppContext { - const app_memory = process.memoryUsage().rss; - const app_start_time = new Date(Date.now() - process.uptime() * 1000).toISOString(); - - return { app_start_time, app_memory }; -} - -/** - * Gets device information from os - */ -export function getDeviceContext(deviceOpt: DeviceContextOptions | true): DeviceContext { - const device: DeviceContext = {}; - - // Sometimes os.uptime() throws due to lacking permissions: https://github.com/getsentry/sentry-javascript/issues/8202 - let uptime; - try { - uptime = os.uptime && os.uptime(); - } catch (e) { - // noop - } - - // os.uptime or its return value seem to be undefined in certain environments (e.g. Azure functions). - // Hence, we only set boot time, if we get a valid uptime value. - // @see https://github.com/getsentry/sentry-javascript/issues/5856 - if (typeof uptime === 'number') { - device.boot_time = new Date(Date.now() - uptime * 1000).toISOString(); - } - - device.arch = os.arch(); - - if (deviceOpt === true || deviceOpt.memory) { - device.memory_size = os.totalmem(); - device.free_memory = os.freemem(); - } - - if (deviceOpt === true || deviceOpt.cpu) { - const cpuInfo: os.CpuInfo[] | undefined = os.cpus(); - if (cpuInfo && cpuInfo.length) { - const firstCpu = cpuInfo[0]; - - device.processor_count = cpuInfo.length; - device.cpu_description = firstCpu.model; - device.processor_frequency = firstCpu.speed; - } - } - - return device; -} - -/** Mapping of Node's platform names to actual OS names. */ -const PLATFORM_NAMES: { [platform: string]: string } = { - aix: 'IBM AIX', - freebsd: 'FreeBSD', - openbsd: 'OpenBSD', - sunos: 'SunOS', - win32: 'Windows', -}; - -/** Linux version file to check for a distribution. */ -interface DistroFile { - /** The file name, located in `/etc`. */ - name: string; - /** Potential distributions to check. */ - distros: string[]; -} - -/** Mapping of linux release files located in /etc to distributions. */ -const LINUX_DISTROS: DistroFile[] = [ - { name: 'fedora-release', distros: ['Fedora'] }, - { name: 'redhat-release', distros: ['Red Hat Linux', 'Centos'] }, - { name: 'redhat_version', distros: ['Red Hat Linux'] }, - { name: 'SuSE-release', distros: ['SUSE Linux'] }, - { name: 'lsb-release', distros: ['Ubuntu Linux', 'Arch Linux'] }, - { name: 'debian_version', distros: ['Debian'] }, - { name: 'debian_release', distros: ['Debian'] }, - { name: 'arch-release', distros: ['Arch Linux'] }, - { name: 'gentoo-release', distros: ['Gentoo Linux'] }, - { name: 'novell-release', distros: ['SUSE Linux'] }, - { name: 'alpine-release', distros: ['Alpine Linux'] }, -]; - -/** Functions to extract the OS version from Linux release files. */ -const LINUX_VERSIONS: { - [identifier: string]: (content: string) => string | undefined; -} = { - alpine: content => content, - arch: content => matchFirst(/distrib_release=(.*)/, content), - centos: content => matchFirst(/release ([^ ]+)/, content), - debian: content => content, - fedora: content => matchFirst(/release (..)/, content), - mint: content => matchFirst(/distrib_release=(.*)/, content), - red: content => matchFirst(/release ([^ ]+)/, content), - suse: content => matchFirst(/VERSION = (.*)\n/, content), - ubuntu: content => matchFirst(/distrib_release=(.*)/, content), -}; - -/** - * Executes a regular expression with one capture group. - * - * @param regex A regular expression to execute. - * @param text Content to execute the RegEx on. - * @returns The captured string if matched; otherwise undefined. - */ -function matchFirst(regex: RegExp, text: string): string | undefined { - const match = regex.exec(text); - return match ? match[1] : undefined; -} - -/** Loads the macOS operating system context. */ -async function getDarwinInfo(): Promise { - // Default values that will be used in case no operating system information - // can be loaded. The default version is computed via heuristics from the - // kernel version, but the build ID is missing. - const darwinInfo: OsContext = { - kernel_version: os.release(), - name: 'Mac OS X', - version: `10.${Number(os.release().split('.')[0]) - 4}`, - }; - - try { - // We try to load the actual macOS version by executing the `sw_vers` tool. - // This tool should be available on every standard macOS installation. In - // case this fails, we stick with the values computed above. - - const output = await new Promise((resolve, reject) => { - execFile('/usr/bin/sw_vers', (error: Error | null, stdout: string) => { - if (error) { - reject(error); - return; - } - resolve(stdout); - }); - }); - - darwinInfo.name = matchFirst(/^ProductName:\s+(.*)$/m, output); - darwinInfo.version = matchFirst(/^ProductVersion:\s+(.*)$/m, output); - darwinInfo.build = matchFirst(/^BuildVersion:\s+(.*)$/m, output); - } catch (e) { - // ignore - } - - return darwinInfo; -} - -/** Returns a distribution identifier to look up version callbacks. */ -function getLinuxDistroId(name: string): string { - return name.split(' ')[0].toLowerCase(); -} - -/** Loads the Linux operating system context. */ -async function getLinuxInfo(): Promise { - // By default, we cannot assume anything about the distribution or Linux - // version. `os.release()` returns the kernel version and we assume a generic - // "Linux" name, which will be replaced down below. - const linuxInfo: OsContext = { - kernel_version: os.release(), - name: 'Linux', - }; - - try { - // We start guessing the distribution by listing files in the /etc - // directory. This is were most Linux distributions (except Knoppix) store - // release files with certain distribution-dependent meta data. We search - // for exactly one known file defined in `LINUX_DISTROS` and exit if none - // are found. In case there are more than one file, we just stick with the - // first one. - const etcFiles = await readDirAsync('/etc'); - const distroFile = LINUX_DISTROS.find(file => etcFiles.includes(file.name)); - if (!distroFile) { - return linuxInfo; - } - - // Once that file is known, load its contents. To make searching in those - // files easier, we lowercase the file contents. Since these files are - // usually quite small, this should not allocate too much memory and we only - // hold on to it for a very short amount of time. - const distroPath = join('/etc', distroFile.name); - const contents = ((await readFileAsync(distroPath, { encoding: 'utf-8' })) as string).toLowerCase(); - - // Some Linux distributions store their release information in the same file - // (e.g. RHEL and Centos). In those cases, we scan the file for an - // identifier, that basically consists of the first word of the linux - // distribution name (e.g. "red" for Red Hat). In case there is no match, we - // just assume the first distribution in our list. - const { distros } = distroFile; - linuxInfo.name = distros.find(d => contents.indexOf(getLinuxDistroId(d)) >= 0) || distros[0]; - - // Based on the found distribution, we can now compute the actual version - // number. This is different for every distribution, so several strategies - // are computed in `LINUX_VERSIONS`. - const id = getLinuxDistroId(linuxInfo.name); - linuxInfo.version = LINUX_VERSIONS[id](contents); - } catch (e) { - // ignore - } - - return linuxInfo; -} - -/** - * Grabs some information about hosting provider based on best effort. - */ -function getCloudResourceContext(): CloudResourceContext | undefined { - if (process.env.VERCEL) { - // https://vercel.com/docs/concepts/projects/environment-variables/system-environment-variables#system-environment-variables - return { - 'cloud.provider': 'vercel', - 'cloud.region': process.env.VERCEL_REGION, - }; - } else if (process.env.AWS_REGION) { - // https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html - return { - 'cloud.provider': 'aws', - 'cloud.region': process.env.AWS_REGION, - 'cloud.platform': process.env.AWS_EXECUTION_ENV, - }; - } else if (process.env.GCP_PROJECT) { - // https://cloud.google.com/composer/docs/how-to/managing/environment-variables#reserved_variables - return { - 'cloud.provider': 'gcp', - }; - } else if (process.env.ALIYUN_REGION_ID) { - // TODO: find where I found these environment variables - at least gc.github.com returns something - return { - 'cloud.provider': 'alibaba_cloud', - 'cloud.region': process.env.ALIYUN_REGION_ID, - }; - } else if (process.env.WEBSITE_SITE_NAME && process.env.REGION_NAME) { - // https://learn.microsoft.com/en-us/azure/app-service/reference-app-settings?tabs=kudu%2Cdotnet#app-environment - return { - 'cloud.provider': 'azure', - 'cloud.region': process.env.REGION_NAME, - }; - } else if (process.env.IBM_CLOUD_REGION) { - // TODO: find where I found these environment variables - at least gc.github.com returns something - return { - 'cloud.provider': 'ibm_cloud', - 'cloud.region': process.env.IBM_CLOUD_REGION, - }; - } else if (process.env.TENCENTCLOUD_REGION) { - // https://www.tencentcloud.com/document/product/583/32748 - return { - 'cloud.provider': 'tencent_cloud', - 'cloud.region': process.env.TENCENTCLOUD_REGION, - 'cloud.account.id': process.env.TENCENTCLOUD_APPID, - 'cloud.availability_zone': process.env.TENCENTCLOUD_ZONE, - }; - } else if (process.env.NETLIFY) { - // https://docs.netlify.com/configure-builds/environment-variables/#read-only-variables - return { - 'cloud.provider': 'netlify', - }; - } else if (process.env.FLY_REGION) { - // https://fly.io/docs/reference/runtime-environment/ - return { - 'cloud.provider': 'fly.io', - 'cloud.region': process.env.FLY_REGION, - }; - } else if (process.env.DYNO) { - // https://devcenter.heroku.com/articles/dynos#local-environment-variables - return { - 'cloud.provider': 'heroku', - }; - } else { - return undefined; - } -} diff --git a/packages/node-experimental/src/integrations/contextlines.ts b/packages/node-experimental/src/integrations/contextlines.ts deleted file mode 100644 index 3755e164e5ea..000000000000 --- a/packages/node-experimental/src/integrations/contextlines.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { promises } from 'fs'; -import { defineIntegration } from '@sentry/core'; -import type { Event, IntegrationFn, StackFrame } from '@sentry/types'; -import { LRUMap, addContextToFrame } from '@sentry/utils'; - -const FILE_CONTENT_CACHE = new LRUMap(100); -const DEFAULT_LINES_OF_CONTEXT = 7; -const INTEGRATION_NAME = 'ContextLines'; - -const readFileAsync = promises.readFile; - -/** - * Resets the file cache. Exists for testing purposes. - * @hidden - */ -export function resetFileContentCache(): void { - FILE_CONTENT_CACHE.clear(); -} - -interface ContextLinesOptions { - /** - * Sets the number of context lines for each frame when loading a file. - * Defaults to 7. - * - * Set to 0 to disable loading and inclusion of source files. - **/ - frameContextLines?: number; -} - -/** Exported only for tests, as a type-safe variant. */ -export const _contextLinesIntegration = ((options: ContextLinesOptions = {}) => { - const contextLines = options.frameContextLines !== undefined ? options.frameContextLines : DEFAULT_LINES_OF_CONTEXT; - - return { - name: INTEGRATION_NAME, - processEvent(event) { - return addSourceContext(event, contextLines); - }, - }; -}) satisfies IntegrationFn; - -/** - * Capture the lines before and after the frame's context. - */ -export const contextLinesIntegration = defineIntegration(_contextLinesIntegration); - -async function addSourceContext(event: Event, contextLines: number): Promise { - // keep a lookup map of which files we've already enqueued to read, - // so we don't enqueue the same file multiple times which would cause multiple i/o reads - const enqueuedReadSourceFileTasks: Record = {}; - const readSourceFileTasks: Promise[] = []; - - if (contextLines > 0 && event.exception?.values) { - for (const exception of event.exception.values) { - if (!exception.stacktrace?.frames) { - continue; - } - - // We want to iterate in reverse order as calling cache.get will bump the file in our LRU cache. - // This ends up prioritizes source context for frames at the top of the stack instead of the bottom. - for (let i = exception.stacktrace.frames.length - 1; i >= 0; i--) { - const frame = exception.stacktrace.frames[i]; - // Call cache.get to bump the file to the top of the cache and ensure we have not already - // enqueued a read operation for this filename - if (frame.filename && !enqueuedReadSourceFileTasks[frame.filename] && !FILE_CONTENT_CACHE.get(frame.filename)) { - readSourceFileTasks.push(_readSourceFile(frame.filename)); - enqueuedReadSourceFileTasks[frame.filename] = 1; - } - } - } - } - - // check if files to read > 0, if so, await all of them to be read before adding source contexts. - // Normally, Promise.all here could be short circuited if one of the promises rejects, but we - // are guarding from that by wrapping the i/o read operation in a try/catch. - if (readSourceFileTasks.length > 0) { - await Promise.all(readSourceFileTasks); - } - - // Perform the same loop as above, but this time we can assume all files are in the cache - // and attempt to add source context to frames. - if (contextLines > 0 && event.exception?.values) { - for (const exception of event.exception.values) { - if (exception.stacktrace && exception.stacktrace.frames) { - await addSourceContextToFrames(exception.stacktrace.frames, contextLines); - } - } - } - - return event; -} - -/** Adds context lines to frames */ -function addSourceContextToFrames(frames: StackFrame[], contextLines: number): void { - for (const frame of frames) { - // Only add context if we have a filename and it hasn't already been added - if (frame.filename && frame.context_line === undefined) { - const sourceFileLines = FILE_CONTENT_CACHE.get(frame.filename); - - if (sourceFileLines) { - try { - addContextToFrame(sourceFileLines, frame, contextLines); - } catch (e) { - // anomaly, being defensive in case - // unlikely to ever happen in practice but can definitely happen in theory - } - } - } - } -} - -/** - * Reads file contents and caches them in a global LRU cache. - * If reading fails, mark the file as null in the cache so we don't try again. - * - * @param filename filepath to read content from. - */ -async function _readSourceFile(filename: string): Promise { - const cachedFile = FILE_CONTENT_CACHE.get(filename); - - // We have already attempted to read this file and failed, do not try again - if (cachedFile === null) { - return null; - } - - // We have a cache hit, return it - if (cachedFile !== undefined) { - return cachedFile; - } - - // Guard from throwing if readFile fails, this enables us to use Promise.all and - // not have it short circuiting if one of the promises rejects + since context lines are added - // on a best effort basis, we want to throw here anyways. - - // If we made it to here, it means that our file is not cache nor marked as failed, so attempt to read it - let content: string[] | null = null; - try { - const rawFileContents = await readFileAsync(filename, 'utf-8'); - content = rawFileContents.split('\n'); - } catch (_) { - // if we fail, we will mark the file as null in the cache and short circuit next time we try to read it - } - - FILE_CONTENT_CACHE.set(filename, content); - return content; -} diff --git a/packages/node-experimental/src/integrations/http.ts b/packages/node-experimental/src/integrations/http.ts deleted file mode 100644 index ed93ebacdaa5..000000000000 --- a/packages/node-experimental/src/integrations/http.ts +++ /dev/null @@ -1,155 +0,0 @@ -import type { ServerResponse } from 'http'; -import type { Span } from '@opentelemetry/api'; -import { SpanKind } from '@opentelemetry/api'; -import { registerInstrumentations } from '@opentelemetry/instrumentation'; -import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; - -import { addBreadcrumb, defineIntegration, getIsolationScope, isSentryRequestUrl } from '@sentry/core'; -import { _INTERNAL, getClient, getSpanKind } from '@sentry/opentelemetry'; -import type { IntegrationFn } from '@sentry/types'; - -import type { NodeClient } from '../sdk/client'; -import { setIsolationScope } from '../sdk/scope'; -import type { HTTPModuleRequestIncomingMessage } from '../transports/http-module'; -import { addOriginToSpan } from '../utils/addOriginToSpan'; -import { getRequestUrl } from '../utils/getRequestUrl'; - -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; - - /** - * Do not capture spans or breadcrumbs for incoming 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. - */ - ignoreIncomingRequests?: (url: string) => boolean; -} - -const _httpIntegration = ((options: HttpOptions = {}) => { - const _breadcrumbs = typeof options.breadcrumbs === 'undefined' ? true : options.breadcrumbs; - const _ignoreOutgoingRequests = options.ignoreOutgoingRequests; - const _ignoreIncomingRequests = options.ignoreIncomingRequests; - - return { - name: 'Http', - setupOnce() { - const instrumentations = [ - new HttpInstrumentation({ - ignoreOutgoingRequestHook: request => { - const url = getRequestUrl(request); - - if (!url) { - return false; - } - - if (isSentryRequestUrl(url, getClient())) { - return true; - } - - if (_ignoreOutgoingRequests && _ignoreOutgoingRequests(url)) { - return true; - } - - return false; - }, - - ignoreIncomingRequestHook: request => { - const url = getRequestUrl(request); - - const method = request.method?.toUpperCase(); - // We do not capture OPTIONS/HEAD requests as transactions - if (method === 'OPTIONS' || method === 'HEAD') { - return true; - } - - if (_ignoreIncomingRequests && _ignoreIncomingRequests(url)) { - return true; - } - - return false; - }, - - requireParentforOutgoingSpans: true, - requireParentforIncomingSpans: false, - requestHook: (span, req) => { - _updateSpan(span); - - // Update the isolation scope, isolate this request - if (getSpanKind(span) === SpanKind.SERVER) { - const isolationScope = getIsolationScope().clone(); - isolationScope.setSDKProcessingMetadata({ request: req }); - - const client = getClient(); - if (client && client.getOptions().autoSessionTracking) { - isolationScope.setRequestSession({ status: 'ok' }); - } - setIsolationScope(isolationScope); - } - }, - responseHook: (span, res) => { - if (_breadcrumbs) { - _addRequestBreadcrumb(span, res); - } - - const client = getClient(); - if (client && client.getOptions().autoSessionTracking) { - setImmediate(() => { - client['_captureRequestSession'](); - }); - } - }, - }), - ]; - - registerInstrumentations({ - instrumentations, - }); - }, - }; -}) satisfies IntegrationFn; - -/** - * The http integration instruments Node's internal http and https modules. - * It creates breadcrumbs and spans for outgoing HTTP requests which will be attached to the currently active span. - */ -export const httpIntegration = defineIntegration(_httpIntegration); - -/** Update the span with data we need. */ -function _updateSpan(span: Span): void { - addOriginToSpan(span, 'auto.http.otel.http'); -} - -/** Add a breadcrumb for outgoing requests. */ -function _addRequestBreadcrumb(span: Span, response: HTTPModuleRequestIncomingMessage | ServerResponse): void { - if (getSpanKind(span) !== SpanKind.CLIENT) { - return; - } - - const data = _INTERNAL.getRequestSpanData(span); - addBreadcrumb( - { - category: 'http', - data: { - status_code: response.statusCode, - ...data, - }, - type: 'http', - }, - { - event: 'response', - // TODO FN: Do we need access to `request` here? - // If we do, we'll have to use the `applyCustomAttributesOnSpan` hook instead, - // but this has worse context semantics than request/responseHook. - response, - }, - ); -} diff --git a/packages/node-experimental/src/integrations/local-variables/common.ts b/packages/node-experimental/src/integrations/local-variables/common.ts deleted file mode 100644 index 3ffee8c0a824..000000000000 --- a/packages/node-experimental/src/integrations/local-variables/common.ts +++ /dev/null @@ -1,119 +0,0 @@ -import type { StackFrame, StackParser } from '@sentry/types'; -import type { Debugger } from 'inspector'; - -export type Variables = Record; - -export type RateLimitIncrement = () => void; - -/** - * Creates a rate limiter that will call the disable callback when the rate limit is reached and the enable callback - * when a timeout has occurred. - * @param maxPerSecond Maximum number of calls per second - * @param enable Callback to enable capture - * @param disable Callback to disable capture - * @returns A function to call to increment the rate limiter count - */ -export function createRateLimiter( - maxPerSecond: number, - enable: () => void, - disable: (seconds: number) => void, -): RateLimitIncrement { - let count = 0; - let retrySeconds = 5; - let disabledTimeout = 0; - - setInterval(() => { - if (disabledTimeout === 0) { - if (count > maxPerSecond) { - retrySeconds *= 2; - disable(retrySeconds); - - // Cap at one day - if (retrySeconds > 86400) { - retrySeconds = 86400; - } - disabledTimeout = retrySeconds; - } - } else { - disabledTimeout -= 1; - - if (disabledTimeout === 0) { - enable(); - } - } - - count = 0; - }, 1_000).unref(); - - return () => { - count += 1; - }; -} - -// Add types for the exception event data -export type PausedExceptionEvent = Debugger.PausedEventDataType & { - data: { - // This contains error.stack - description: string; - }; -}; - -/** Could this be an anonymous function? */ -export function isAnonymous(name: string | undefined): boolean { - return name !== undefined && (name.length === 0 || name === '?' || name === ''); -} - -/** Do the function names appear to match? */ -export function functionNamesMatch(a: string | undefined, b: string | undefined): boolean { - return a === b || (isAnonymous(a) && isAnonymous(b)); -} - -/** Creates a unique hash from stack frames */ -export function hashFrames(frames: StackFrame[] | undefined): string | undefined { - if (frames === undefined) { - return; - } - - // Only hash the 10 most recent frames (ie. the last 10) - return frames.slice(-10).reduce((acc, frame) => `${acc},${frame.function},${frame.lineno},${frame.colno}`, ''); -} - -/** - * We use the stack parser to create a unique hash from the exception stack trace - * This is used to lookup vars when the exception passes through the event processor - */ -export function hashFromStack(stackParser: StackParser, stack: string | undefined): string | undefined { - if (stack === undefined) { - return undefined; - } - - return hashFrames(stackParser(stack, 1)); -} - -export interface FrameVariables { - function: string; - vars?: Variables; -} - -export interface LocalVariablesIntegrationOptions { - /** - * Capture local variables for both caught and uncaught exceptions - * - * - When false, only uncaught exceptions will have local variables - * - When true, both caught and uncaught exceptions will have local variables. - * - * Defaults to `true`. - * - * Capturing local variables for all exceptions can be expensive since the debugger pauses for every throw to collect - * local variables. - * - * To reduce the likelihood of this feature impacting app performance or throughput, this feature is rate-limited. - * Once the rate limit is reached, local variables will only be captured for uncaught exceptions until a timeout has - * been reached. - */ - captureAllExceptions?: boolean; - /** - * Maximum number of exceptions to capture local variables for per second before rate limiting is triggered. - */ - maxExceptionsPerSecond?: number; -} diff --git a/packages/node-experimental/src/integrations/local-variables/index.ts b/packages/node-experimental/src/integrations/local-variables/index.ts deleted file mode 100644 index 60649b03118f..000000000000 --- a/packages/node-experimental/src/integrations/local-variables/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { localVariablesSyncIntegration } from './local-variables-sync'; - -export const localVariablesIntegration = localVariablesSyncIntegration; diff --git a/packages/node-experimental/src/integrations/local-variables/local-variables-sync.ts b/packages/node-experimental/src/integrations/local-variables/local-variables-sync.ts deleted file mode 100644 index 91fb9005b4c3..000000000000 --- a/packages/node-experimental/src/integrations/local-variables/local-variables-sync.ts +++ /dev/null @@ -1,392 +0,0 @@ -import { defineIntegration, getClient } from '@sentry/core'; -import type { Event, Exception, IntegrationFn, StackParser } from '@sentry/types'; -import { LRUMap, logger } from '@sentry/utils'; -import type { Debugger, InspectorNotification, Runtime } from 'inspector'; -import { Session } from 'inspector'; - -import { NODE_MAJOR } from '../../nodeVersion'; -import type { NodeClient } from '../../sdk/client'; -import type { - FrameVariables, - LocalVariablesIntegrationOptions, - PausedExceptionEvent, - RateLimitIncrement, - Variables, -} from './common'; -import { createRateLimiter, functionNamesMatch, hashFrames, hashFromStack } from './common'; - -type OnPauseEvent = InspectorNotification; -export interface DebugSession { - /** Configures and connects to the debug session */ - configureAndConnect(onPause: (message: OnPauseEvent, complete: () => void) => void, captureAll: boolean): void; - /** Updates which kind of exceptions to capture */ - setPauseOnExceptions(captureAll: boolean): void; - /** Gets local variables for an objectId */ - getLocalVariables(objectId: string, callback: (vars: Variables) => void): void; -} - -type Next = (result: T) => void; -type Add = (fn: Next) => void; -type CallbackWrapper = { add: Add; next: Next }; - -/** Creates a container for callbacks to be called sequentially */ -export function createCallbackList(complete: Next): CallbackWrapper { - // A collection of callbacks to be executed last to first - let callbacks: Next[] = []; - - let completedCalled = false; - function checkedComplete(result: T): void { - callbacks = []; - if (completedCalled) { - return; - } - completedCalled = true; - complete(result); - } - - // complete should be called last - callbacks.push(checkedComplete); - - function add(fn: Next): void { - callbacks.push(fn); - } - - function next(result: T): void { - const popped = callbacks.pop() || checkedComplete; - - try { - popped(result); - } catch (_) { - // If there is an error, we still want to call the complete callback - checkedComplete(result); - } - } - - return { add, next }; -} - -/** - * Promise API is available as `Experimental` and in Node 19 only. - * - * Callback-based API is `Stable` since v14 and `Experimental` since v8. - * Because of that, we are creating our own `AsyncSession` class. - * - * https://nodejs.org/docs/latest-v19.x/api/inspector.html#promises-api - * https://nodejs.org/docs/latest-v14.x/api/inspector.html - */ -class AsyncSession implements DebugSession { - private readonly _session: Session; - - /** Throws if inspector API is not available */ - public constructor() { - this._session = new Session(); - } - - /** @inheritdoc */ - public configureAndConnect(onPause: (event: OnPauseEvent, complete: () => void) => void, captureAll: boolean): void { - this._session.connect(); - - this._session.on('Debugger.paused', event => { - onPause(event, () => { - // After the pause work is complete, resume execution or the exception context memory is leaked - this._session.post('Debugger.resume'); - }); - }); - - this._session.post('Debugger.enable'); - this._session.post('Debugger.setPauseOnExceptions', { state: captureAll ? 'all' : 'uncaught' }); - } - - public setPauseOnExceptions(captureAll: boolean): void { - this._session.post('Debugger.setPauseOnExceptions', { state: captureAll ? 'all' : 'uncaught' }); - } - - /** @inheritdoc */ - public getLocalVariables(objectId: string, complete: (vars: Variables) => void): void { - this._getProperties(objectId, props => { - const { add, next } = createCallbackList(complete); - - for (const prop of props) { - if (prop?.value?.objectId && prop?.value.className === 'Array') { - const id = prop.value.objectId; - add(vars => this._unrollArray(id, prop.name, vars, next)); - } else if (prop?.value?.objectId && prop?.value?.className === 'Object') { - const id = prop.value.objectId; - add(vars => this._unrollObject(id, prop.name, vars, next)); - } else if (prop?.value) { - add(vars => this._unrollOther(prop, vars, next)); - } - } - - next({}); - }); - } - - /** - * Gets all the PropertyDescriptors of an object - */ - private _getProperties(objectId: string, next: (result: Runtime.PropertyDescriptor[]) => void): void { - this._session.post( - 'Runtime.getProperties', - { - objectId, - ownProperties: true, - }, - (err, params) => { - if (err) { - next([]); - } else { - next(params.result); - } - }, - ); - } - - /** - * Unrolls an array property - */ - private _unrollArray(objectId: string, name: string, vars: Variables, next: (vars: Variables) => void): void { - this._getProperties(objectId, props => { - vars[name] = props - .filter(v => v.name !== 'length' && !isNaN(parseInt(v.name, 10))) - .sort((a, b) => parseInt(a.name, 10) - parseInt(b.name, 10)) - .map(v => v?.value?.value); - - next(vars); - }); - } - - /** - * Unrolls an object property - */ - private _unrollObject(objectId: string, name: string, vars: Variables, next: (obj: Variables) => void): void { - this._getProperties(objectId, props => { - vars[name] = props - .map<[string, unknown]>(v => [v.name, v?.value?.value]) - .reduce((obj, [key, val]) => { - obj[key] = val; - return obj; - }, {} as Variables); - - next(vars); - }); - } - - /** - * Unrolls other properties - */ - private _unrollOther(prop: Runtime.PropertyDescriptor, vars: Variables, next: (vars: Variables) => void): void { - if (prop.value) { - if ('value' in prop.value) { - if (prop.value.value === undefined || prop.value.value === null) { - vars[prop.name] = `<${prop.value.value}>`; - } else { - vars[prop.name] = prop.value.value; - } - } else if ('description' in prop.value && prop.value.type !== 'function') { - vars[prop.name] = `<${prop.value.description}>`; - } else if (prop.value.type === 'undefined') { - vars[prop.name] = ''; - } - } - - next(vars); - } -} - -/** - * When using Vercel pkg, the inspector module is not available. - * https://github.com/getsentry/sentry-javascript/issues/6769 - */ -function tryNewAsyncSession(): AsyncSession | undefined { - try { - return new AsyncSession(); - } catch (e) { - return undefined; - } -} - -const INTEGRATION_NAME = 'LocalVariables'; - -/** - * Adds local variables to exception frames - */ -const _localVariablesSyncIntegration = (( - options: LocalVariablesIntegrationOptions = {}, - session: DebugSession | undefined = tryNewAsyncSession(), -) => { - const cachedFrames: LRUMap = new LRUMap(20); - let rateLimiter: RateLimitIncrement | undefined; - let shouldProcessEvent = false; - - function handlePaused( - stackParser: StackParser, - { params: { reason, data, callFrames } }: InspectorNotification, - complete: () => void, - ): void { - if (reason !== 'exception' && reason !== 'promiseRejection') { - complete(); - return; - } - - rateLimiter?.(); - - // data.description contains the original error.stack - const exceptionHash = hashFromStack(stackParser, data?.description); - - if (exceptionHash == undefined) { - complete(); - return; - } - - const { add, next } = createCallbackList(frames => { - cachedFrames.set(exceptionHash, frames); - complete(); - }); - - // Because we're queuing up and making all these calls synchronously, we can potentially overflow the stack - // For this reason we only attempt to get local variables for the first 5 frames - for (let i = 0; i < Math.min(callFrames.length, 5); i++) { - const { scopeChain, functionName, this: obj } = callFrames[i]; - - const localScope = scopeChain.find(scope => scope.type === 'local'); - - // obj.className is undefined in ESM modules - const fn = obj.className === 'global' || !obj.className ? functionName : `${obj.className}.${functionName}`; - - if (localScope?.object.objectId === undefined) { - add(frames => { - frames[i] = { function: fn }; - next(frames); - }); - } else { - const id = localScope.object.objectId; - add(frames => - session?.getLocalVariables(id, vars => { - frames[i] = { function: fn, vars }; - next(frames); - }), - ); - } - } - - next([]); - } - - function addLocalVariablesToException(exception: Exception): void { - const hash = hashFrames(exception?.stacktrace?.frames); - - if (hash === undefined) { - return; - } - - // Check if we have local variables for an exception that matches the hash - // remove is identical to get but also removes the entry from the cache - const cachedFrame = cachedFrames.remove(hash); - - if (cachedFrame === undefined) { - return; - } - - // Filter out frames where the function name is `new Promise` since these are in the error.stack frames - // but do not appear in the debugger call frames - const frames = (exception.stacktrace?.frames || []).filter(frame => frame.function !== 'new Promise'); - - for (let i = 0; i < frames.length; i++) { - // Sentry frames are in reverse order - const frameIndex = frames.length - i - 1; - - // Drop out if we run out of frames to match up - if (!frames[frameIndex] || !cachedFrame[i]) { - break; - } - - if ( - // We need to have vars to add - cachedFrame[i].vars === undefined || - // We're not interested in frames that are not in_app because the vars are not relevant - frames[frameIndex].in_app === false || - // The function names need to match - !functionNamesMatch(frames[frameIndex].function, cachedFrame[i].function) - ) { - continue; - } - - frames[frameIndex].vars = cachedFrame[i].vars; - } - } - - function addLocalVariablesToEvent(event: Event): Event { - for (const exception of event?.exception?.values || []) { - addLocalVariablesToException(exception); - } - - return event; - } - - return { - name: INTEGRATION_NAME, - setupOnce() { - const client = getClient(); - const clientOptions = client?.getOptions(); - - if (session && clientOptions?.includeLocalVariables) { - // Only setup this integration if the Node version is >= v18 - // https://github.com/getsentry/sentry-javascript/issues/7697 - const unsupportedNodeVersion = NODE_MAJOR < 18; - - if (unsupportedNodeVersion) { - logger.log('The `LocalVariables` integration is only supported on Node >= v18.'); - return; - } - - const captureAll = options.captureAllExceptions !== false; - - session.configureAndConnect( - (ev, complete) => - handlePaused(clientOptions.stackParser, ev as InspectorNotification, complete), - captureAll, - ); - - if (captureAll) { - const max = options.maxExceptionsPerSecond || 50; - - rateLimiter = createRateLimiter( - max, - () => { - logger.log('Local variables rate-limit lifted.'); - session?.setPauseOnExceptions(true); - }, - seconds => { - logger.log( - `Local variables rate-limit exceeded. Disabling capturing of caught exceptions for ${seconds} seconds.`, - ); - session?.setPauseOnExceptions(false); - }, - ); - } - - shouldProcessEvent = true; - } - }, - processEvent(event: Event): Event { - if (shouldProcessEvent) { - return addLocalVariablesToEvent(event); - } - - return event; - }, - // These are entirely for testing - _getCachedFramesCount(): number { - return cachedFrames.size; - }, - _getFirstCachedFrame(): FrameVariables[] | undefined { - return cachedFrames.values()[0]; - }, - }; -}) satisfies IntegrationFn; - -/** - * Adds local variables to exception frames. - */ -export const localVariablesSyncIntegration = defineIntegration(_localVariablesSyncIntegration); diff --git a/packages/node-experimental/src/integrations/modules.ts b/packages/node-experimental/src/integrations/modules.ts deleted file mode 100644 index ad30bb4d7a3b..000000000000 --- a/packages/node-experimental/src/integrations/modules.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { existsSync, readFileSync } from 'fs'; -import { dirname, join } from 'path'; -import { defineIntegration } from '@sentry/core'; -import type { IntegrationFn } from '@sentry/types'; - -let moduleCache: { [key: string]: string }; - -const INTEGRATION_NAME = 'Modules'; - -const _modulesIntegration = (() => { - return { - name: INTEGRATION_NAME, - processEvent(event) { - event.modules = { - ...event.modules, - ..._getModules(), - }; - - return event; - }, - }; -}) satisfies IntegrationFn; - -/** - * Add node modules / packages to the event. - */ -export const modulesIntegration = defineIntegration(_modulesIntegration); - -/** Extract information about paths */ -function getPaths(): string[] { - try { - return require.cache ? Object.keys(require.cache as Record) : []; - } catch (e) { - return []; - } -} - -/** Extract information about package.json modules */ -function collectModules(): { - [name: string]: string; -} { - const mainPaths = (require.main && require.main.paths) || []; - const paths = getPaths(); - const infos: { - [name: string]: string; - } = {}; - const seen: { - [path: string]: boolean; - } = {}; - - paths.forEach(path => { - let dir = path; - - /** Traverse directories upward in the search of package.json file */ - const updir = (): void | (() => void) => { - const orig = dir; - dir = dirname(orig); - - if (!dir || orig === dir || seen[orig]) { - return undefined; - } - if (mainPaths.indexOf(dir) < 0) { - return updir(); - } - - const pkgfile = join(orig, 'package.json'); - seen[orig] = true; - - if (!existsSync(pkgfile)) { - return updir(); - } - - try { - const info = JSON.parse(readFileSync(pkgfile, 'utf8')) as { - name: string; - version: string; - }; - infos[info.name] = info.version; - } catch (_oO) { - // no-empty - } - }; - - updir(); - }); - - return infos; -} - -/** Fetches the list of modules and the versions loaded by the entry file for your node.js app. */ -function _getModules(): { [key: string]: string } { - if (!moduleCache) { - moduleCache = collectModules(); - } - return moduleCache; -} diff --git a/packages/node-experimental/src/integrations/onuncaughtexception.ts b/packages/node-experimental/src/integrations/onuncaughtexception.ts deleted file mode 100644 index e56c3c0801d7..000000000000 --- a/packages/node-experimental/src/integrations/onuncaughtexception.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { captureException, defineIntegration } from '@sentry/core'; -import { getClient } from '@sentry/core'; -import type { IntegrationFn } from '@sentry/types'; -import { logger } from '@sentry/utils'; - -import { DEBUG_BUILD } from '../debug-build'; -import type { NodeClient } from '../sdk/client'; -import { logAndExitProcess } from '../utils/errorhandling'; - -type OnFatalErrorHandler = (firstError: Error, secondError?: Error) => void; - -type TaggedListener = NodeJS.UncaughtExceptionListener & { - tag?: string; -}; - -// CAREFUL: Please think twice before updating the way _options looks because the Next.js SDK depends on it in `index.server.ts` -interface OnUncaughtExceptionOptions { - // TODO(v8): Evaluate whether we should switch the default behaviour here. - // Also, we can evaluate using https://nodejs.org/api/process.html#event-uncaughtexceptionmonitor per default, and - // falling back to current behaviour when that's not available. - /** - * Controls if the SDK should register a handler to exit the process on uncaught errors: - * - `true`: The SDK will exit the process on all uncaught errors. - * - `false`: The SDK will only exit the process when there are no other `uncaughtException` handlers attached. - * - * Default: `true` - */ - exitEvenIfOtherHandlersAreRegistered: boolean; - - /** - * This is called when an uncaught error would cause the process to exit. - * - * @param firstError Uncaught error causing the process to exit - * @param secondError Will be set if the handler was called multiple times. This can happen either because - * `onFatalError` itself threw, or because an independent error happened somewhere else while `onFatalError` - * was running. - */ - onFatalError?(this: void, firstError: Error, secondError?: Error): void; -} - -const INTEGRATION_NAME = 'OnUncaughtException'; - -const _onUncaughtExceptionIntegration = ((options: Partial = {}) => { - const _options = { - exitEvenIfOtherHandlersAreRegistered: true, - ...options, - }; - - return { - name: INTEGRATION_NAME, - setup(client: NodeClient) { - global.process.on('uncaughtException', makeErrorHandler(client, _options)); - }, - }; -}) satisfies IntegrationFn; - -/** - * Add a global exception handler. - */ -export const onUncaughtExceptionIntegration = defineIntegration(_onUncaughtExceptionIntegration); - -type ErrorHandler = { _errorHandler: boolean } & ((error: Error) => void); - -/** Exported only for tests */ -export function makeErrorHandler(client: NodeClient, options: OnUncaughtExceptionOptions): ErrorHandler { - const timeout = 2000; - let caughtFirstError: boolean = false; - let caughtSecondError: boolean = false; - let calledFatalError: boolean = false; - let firstError: Error; - - const clientOptions = client.getOptions(); - - return Object.assign( - (error: Error): void => { - let onFatalError: OnFatalErrorHandler = logAndExitProcess; - - if (options.onFatalError) { - onFatalError = options.onFatalError; - } else if (clientOptions.onFatalError) { - onFatalError = clientOptions.onFatalError as OnFatalErrorHandler; - } - - // Attaching a listener to `uncaughtException` will prevent the node process from exiting. We generally do not - // want to alter this behaviour so we check for other listeners that users may have attached themselves and adjust - // exit behaviour of the SDK accordingly: - // - If other listeners are attached, do not exit. - // - If the only listener attached is ours, exit. - const userProvidedListenersCount = (global.process.listeners('uncaughtException') as TaggedListener[]).filter( - listener => { - // There are 3 listeners we ignore: - return ( - // as soon as we're using domains this listener is attached by node itself - listener.name !== 'domainUncaughtExceptionClear' && - // the handler we register for tracing - listener.tag !== 'sentry_tracingErrorCallback' && - // the handler we register in this integration - (listener as ErrorHandler)._errorHandler !== true - ); - }, - ).length; - - const processWouldExit = userProvidedListenersCount === 0; - const shouldApplyFatalHandlingLogic = options.exitEvenIfOtherHandlersAreRegistered || processWouldExit; - - if (!caughtFirstError) { - // this is the first uncaught error and the ultimate reason for shutting down - // we want to do absolutely everything possible to ensure it gets captured - // also we want to make sure we don't go recursion crazy if more errors happen after this one - firstError = error; - caughtFirstError = true; - - if (getClient() === client) { - captureException(error, { - originalException: error, - captureContext: { - level: 'fatal', - }, - mechanism: { - handled: false, - type: 'onuncaughtexception', - }, - }); - } - - if (!calledFatalError && shouldApplyFatalHandlingLogic) { - calledFatalError = true; - onFatalError(error); - } - } else { - if (shouldApplyFatalHandlingLogic) { - if (calledFatalError) { - // we hit an error *after* calling onFatalError - pretty boned at this point, just shut it down - DEBUG_BUILD && - logger.warn( - 'uncaught exception after calling fatal error shutdown callback - this is bad! forcing shutdown', - ); - logAndExitProcess(error); - } else if (!caughtSecondError) { - // two cases for how we can hit this branch: - // - capturing of first error blew up and we just caught the exception from that - // - quit trying to capture, proceed with shutdown - // - a second independent error happened while waiting for first error to capture - // - want to avoid causing premature shutdown before first error capture finishes - // it's hard to immediately tell case 1 from case 2 without doing some fancy/questionable domain stuff - // so let's instead just delay a bit before we proceed with our action here - // in case 1, we just wait a bit unnecessarily but ultimately do the same thing - // in case 2, the delay hopefully made us wait long enough for the capture to finish - // two potential nonideal outcomes: - // nonideal case 1: capturing fails fast, we sit around for a few seconds unnecessarily before proceeding correctly by calling onFatalError - // nonideal case 2: case 2 happens, 1st error is captured but slowly, timeout completes before capture and we treat second error as the sendErr of (nonexistent) failure from trying to capture first error - // note that after hitting this branch, we might catch more errors where (caughtSecondError && !calledFatalError) - // we ignore them - they don't matter to us, we're just waiting for the second error timeout to finish - caughtSecondError = true; - setTimeout(() => { - if (!calledFatalError) { - // it was probably case 1, let's treat err as the sendErr and call onFatalError - calledFatalError = true; - onFatalError(firstError, error); - } else { - // it was probably case 2, our first error finished capturing while we waited, cool, do nothing - } - }, timeout); // capturing could take at least sendTimeout to fail, plus an arbitrary second for how long it takes to collect surrounding source etc - } - } - } - }, - { _errorHandler: true }, - ); -} diff --git a/packages/node-experimental/src/integrations/onunhandledrejection.ts b/packages/node-experimental/src/integrations/onunhandledrejection.ts deleted file mode 100644 index e1bc0b4145cf..000000000000 --- a/packages/node-experimental/src/integrations/onunhandledrejection.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { captureException, defineIntegration, getClient } from '@sentry/core'; -import type { Client, IntegrationFn } from '@sentry/types'; -import { consoleSandbox } from '@sentry/utils'; -import { logAndExitProcess } from '../utils/errorhandling'; - -type UnhandledRejectionMode = 'none' | 'warn' | 'strict'; - -interface OnUnhandledRejectionOptions { - /** - * Option deciding what to do after capturing unhandledRejection, - * that mimicks behavior of node's --unhandled-rejection flag. - */ - mode: UnhandledRejectionMode; -} - -const INTEGRATION_NAME = 'OnUnhandledRejection'; - -const _onUnhandledRejectionIntegration = ((options: Partial = {}) => { - const mode = options.mode || 'warn'; - - return { - name: INTEGRATION_NAME, - setup(client) { - global.process.on('unhandledRejection', makeUnhandledPromiseHandler(client, { mode })); - }, - }; -}) satisfies IntegrationFn; - -/** - * Add a global promise rejection handler. - */ -export const onUnhandledRejectionIntegration = defineIntegration(_onUnhandledRejectionIntegration); - -/** - * Send an exception with reason - * @param reason string - * @param promise promise - * - * Exported only for tests. - */ -export function makeUnhandledPromiseHandler( - client: Client, - options: OnUnhandledRejectionOptions, -): (reason: unknown, promise: unknown) => void { - return function sendUnhandledPromise(reason: unknown, promise: unknown): void { - if (getClient() !== client) { - return; - } - - captureException(reason, { - originalException: promise, - captureContext: { - extra: { unhandledPromiseRejection: true }, - }, - mechanism: { - handled: false, - type: 'onunhandledrejection', - }, - }); - - handleRejection(reason, options); - }; -} - -/** - * Handler for `mode` option - - */ -function handleRejection( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - reason: any, - options: OnUnhandledRejectionOptions, -): void { - // https://github.com/nodejs/node/blob/7cf6f9e964aa00772965391c23acda6d71972a9a/lib/internal/process/promises.js#L234-L240 - const rejectionWarning = - 'This error originated either by ' + - 'throwing inside of an async function without a catch block, ' + - 'or by rejecting a promise which was not handled with .catch().' + - ' The promise rejected with the reason:'; - - /* eslint-disable no-console */ - if (options.mode === 'warn') { - consoleSandbox(() => { - console.warn(rejectionWarning); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - console.error(reason && reason.stack ? reason.stack : reason); - }); - } else if (options.mode === 'strict') { - consoleSandbox(() => { - console.warn(rejectionWarning); - }); - logAndExitProcess(reason); - } - /* eslint-enable no-console */ -} diff --git a/packages/node-experimental/src/integrations/spotlight.ts b/packages/node-experimental/src/integrations/spotlight.ts deleted file mode 100644 index 21629ad340ac..000000000000 --- a/packages/node-experimental/src/integrations/spotlight.ts +++ /dev/null @@ -1,115 +0,0 @@ -import * as http from 'http'; -import { defineIntegration } from '@sentry/core'; -import type { Client, Envelope, IntegrationFn } from '@sentry/types'; -import { logger, serializeEnvelope } from '@sentry/utils'; - -type SpotlightConnectionOptions = { - /** - * Set this if the Spotlight Sidecar is not running on localhost:8969 - * By default, the Url is set to http://localhost:8969/stream - */ - sidecarUrl?: string; -}; - -const INTEGRATION_NAME = 'Spotlight'; - -const _spotlightIntegration = ((options: Partial = {}) => { - const _options = { - sidecarUrl: options.sidecarUrl || 'http://localhost:8969/stream', - }; - - return { - name: INTEGRATION_NAME, - setup(client) { - if (typeof process === 'object' && process.env && process.env.NODE_ENV !== 'development') { - logger.warn("[Spotlight] It seems you're not in dev mode. Do you really want to have Spotlight enabled?"); - } - connectToSpotlight(client, _options); - }, - }; -}) satisfies IntegrationFn; - -/** - * Use this integration to send errors and transactions to Spotlight. - * - * Learn more about spotlight at https://spotlightjs.com - * - * Important: This integration only works with Node 18 or newer. - */ -export const spotlightIntegration = defineIntegration(_spotlightIntegration); - -function connectToSpotlight(client: Client, options: Required): void { - const spotlightUrl = parseSidecarUrl(options.sidecarUrl); - if (!spotlightUrl) { - return; - } - - let failedRequests = 0; - - client.on('beforeEnvelope', (envelope: Envelope) => { - if (failedRequests > 3) { - logger.warn('[Spotlight] Disabled Sentry -> Spotlight integration due to too many failed requests'); - return; - } - - const serializedEnvelope = serializeEnvelope(envelope); - - const request = getNativeHttpRequest(); - const req = request( - { - method: 'POST', - path: spotlightUrl.pathname, - hostname: spotlightUrl.hostname, - port: spotlightUrl.port, - headers: { - 'Content-Type': 'application/x-sentry-envelope', - }, - }, - res => { - res.on('data', () => { - // Drain socket - }); - - res.on('end', () => { - // Drain socket - }); - res.setEncoding('utf8'); - }, - ); - - req.on('error', () => { - failedRequests++; - logger.warn('[Spotlight] Failed to send envelope to Spotlight Sidecar'); - }); - req.write(serializedEnvelope); - req.end(); - }); -} - -function parseSidecarUrl(url: string): URL | undefined { - try { - return new URL(`${url}`); - } catch { - logger.warn(`[Spotlight] Invalid sidecar URL: ${url}`); - return undefined; - } -} - -type HttpRequestImpl = typeof http.request; -type WrappedHttpRequest = HttpRequestImpl & { __sentry_original__: HttpRequestImpl }; - -/** - * We want to get an unpatched http request implementation to avoid capturing our own calls. - */ -export function getNativeHttpRequest(): HttpRequestImpl { - const { request } = http; - if (isWrapped(request)) { - return request.__sentry_original__; - } - - return request; -} - -function isWrapped(impl: HttpRequestImpl): impl is WrappedHttpRequest { - return '__sentry_original__' in impl; -} diff --git a/packages/node-experimental/src/nodeVersion.ts b/packages/node-experimental/src/nodeVersion.ts deleted file mode 100644 index 1f07883b771b..000000000000 --- a/packages/node-experimental/src/nodeVersion.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { parseSemver } from '@sentry/utils'; - -export const NODE_VERSION = parseSemver(process.versions.node) as { major: number; minor: number; patch: number }; -export const NODE_MAJOR = NODE_VERSION.major; diff --git a/packages/node-experimental/src/proxy/base.ts b/packages/node-experimental/src/proxy/base.ts deleted file mode 100644 index e1ef24c3092e..000000000000 --- a/packages/node-experimental/src/proxy/base.ts +++ /dev/null @@ -1,151 +0,0 @@ -/** - * This code was originally forked from https://github.com/TooTallNate/proxy-agents/tree/b133295fd16f6475578b6b15bd9b4e33ecb0d0b7 - * With the following licence: - * - * (The MIT License) - * - * Copyright (c) 2013 Nathan Rajlich * - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * 'Software'), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions:* - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software.* - * - * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -/* eslint-disable @typescript-eslint/explicit-member-accessibility */ -/* eslint-disable @typescript-eslint/member-ordering */ -/* eslint-disable jsdoc/require-jsdoc */ -import * as http from 'http'; -import type * as net from 'net'; -import type { Duplex } from 'stream'; -import type * as tls from 'tls'; - -export * from './helpers'; - -interface HttpConnectOpts extends net.TcpNetConnectOpts { - secureEndpoint: false; - protocol?: string; -} - -interface HttpsConnectOpts extends tls.ConnectionOptions { - secureEndpoint: true; - protocol?: string; - port: number; -} - -export type AgentConnectOpts = HttpConnectOpts | HttpsConnectOpts; - -const INTERNAL = Symbol('AgentBaseInternalState'); - -interface InternalState { - defaultPort?: number; - protocol?: string; - currentSocket?: Duplex; -} - -export abstract class Agent extends http.Agent { - private [INTERNAL]: InternalState; - - // Set by `http.Agent` - missing from `@types/node` - options!: Partial; - keepAlive!: boolean; - - constructor(opts?: http.AgentOptions) { - super(opts); - this[INTERNAL] = {}; - } - - abstract connect( - req: http.ClientRequest, - options: AgentConnectOpts, - ): Promise | Duplex | http.Agent; - - /** - * Determine whether this is an `http` or `https` request. - */ - isSecureEndpoint(options?: AgentConnectOpts): boolean { - if (options) { - // First check the `secureEndpoint` property explicitly, since this - // means that a parent `Agent` is "passing through" to this instance. - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access - if (typeof (options as any).secureEndpoint === 'boolean') { - return options.secureEndpoint; - } - - // If no explicit `secure` endpoint, check if `protocol` property is - // set. This will usually be the case since using a full string URL - // or `URL` instance should be the most common usage. - if (typeof options.protocol === 'string') { - return options.protocol === 'https:'; - } - } - - // Finally, if no `protocol` property was set, then fall back to - // checking the stack trace of the current call stack, and try to - // detect the "https" module. - const { stack } = new Error(); - if (typeof stack !== 'string') return false; - return stack.split('\n').some(l => l.indexOf('(https.js:') !== -1 || l.indexOf('node:https:') !== -1); - } - - createSocket(req: http.ClientRequest, options: AgentConnectOpts, cb: (err: Error | null, s?: Duplex) => void): void { - const connectOpts = { - ...options, - secureEndpoint: this.isSecureEndpoint(options), - }; - Promise.resolve() - .then(() => this.connect(req, connectOpts)) - .then(socket => { - if (socket instanceof http.Agent) { - // @ts-expect-error `addRequest()` isn't defined in `@types/node` - return socket.addRequest(req, connectOpts); - } - this[INTERNAL].currentSocket = socket; - // @ts-expect-error `createSocket()` isn't defined in `@types/node` - super.createSocket(req, options, cb); - }, cb); - } - - createConnection(): Duplex { - const socket = this[INTERNAL].currentSocket; - this[INTERNAL].currentSocket = undefined; - if (!socket) { - throw new Error('No socket was returned in the `connect()` function'); - } - return socket; - } - - get defaultPort(): number { - return this[INTERNAL].defaultPort ?? (this.protocol === 'https:' ? 443 : 80); - } - - set defaultPort(v: number) { - if (this[INTERNAL]) { - this[INTERNAL].defaultPort = v; - } - } - - get protocol(): string { - return this[INTERNAL].protocol ?? (this.isSecureEndpoint() ? 'https:' : 'http:'); - } - - set protocol(v: string) { - if (this[INTERNAL]) { - this[INTERNAL].protocol = v; - } - } -} diff --git a/packages/node-experimental/src/proxy/helpers.ts b/packages/node-experimental/src/proxy/helpers.ts deleted file mode 100644 index 031878511f6c..000000000000 --- a/packages/node-experimental/src/proxy/helpers.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * This code was originally forked from https://github.com/TooTallNate/proxy-agents/tree/b133295fd16f6475578b6b15bd9b4e33ecb0d0b7 - * With the following licence: - * - * (The MIT License) - * - * Copyright (c) 2013 Nathan Rajlich * - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * 'Software'), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions:* - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software.* - * - * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -/* eslint-disable jsdoc/require-jsdoc */ -import * as http from 'node:http'; -import * as https from 'node:https'; -import type { Readable } from 'stream'; - -export type ThenableRequest = http.ClientRequest & { - then: Promise['then']; -}; - -export async function toBuffer(stream: Readable): Promise { - let length = 0; - const chunks: Buffer[] = []; - for await (const chunk of stream) { - length += (chunk as Buffer).length; - chunks.push(chunk); - } - return Buffer.concat(chunks, length); -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export async function json(stream: Readable): Promise { - const buf = await toBuffer(stream); - const str = buf.toString('utf8'); - try { - return JSON.parse(str); - } catch (_err: unknown) { - const err = _err as Error; - err.message += ` (input: ${str})`; - throw err; - } -} - -export function req(url: string | URL, opts: https.RequestOptions = {}): ThenableRequest { - const href = typeof url === 'string' ? url : url.href; - const req = (href.startsWith('https:') ? https : http).request(url, opts) as ThenableRequest; - const promise = new Promise((resolve, reject) => { - req.once('response', resolve).once('error', reject).end() as unknown as ThenableRequest; - }); - req.then = promise.then.bind(promise); - return req; -} diff --git a/packages/node-experimental/src/proxy/index.ts b/packages/node-experimental/src/proxy/index.ts deleted file mode 100644 index 83f72d56fb4e..000000000000 --- a/packages/node-experimental/src/proxy/index.ts +++ /dev/null @@ -1,221 +0,0 @@ -/** - * This code was originally forked from https://github.com/TooTallNate/proxy-agents/tree/b133295fd16f6475578b6b15bd9b4e33ecb0d0b7 - * With the following licence: - * - * (The MIT License) - * - * Copyright (c) 2013 Nathan Rajlich * - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * 'Software'), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions:* - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software.* - * - * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -/* eslint-disable @typescript-eslint/explicit-member-accessibility */ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import type * as http from 'http'; -import type { OutgoingHttpHeaders } from 'http'; -import * as net from 'net'; -import * as tls from 'tls'; -import { logger } from '@sentry/utils'; -import { Agent } from './base'; -import type { AgentConnectOpts } from './base'; -import { parseProxyResponse } from './parse-proxy-response'; - -function debug(...args: unknown[]): void { - logger.log('[https-proxy-agent]', ...args); -} - -type Protocol = T extends `${infer Protocol}:${infer _}` ? Protocol : never; - -type ConnectOptsMap = { - http: Omit; - https: Omit; -}; - -type ConnectOpts = { - [P in keyof ConnectOptsMap]: Protocol extends P ? ConnectOptsMap[P] : never; -}[keyof ConnectOptsMap]; - -export type HttpsProxyAgentOptions = ConnectOpts & - http.AgentOptions & { - headers?: OutgoingHttpHeaders | (() => OutgoingHttpHeaders); - }; - -/** - * The `HttpsProxyAgent` implements an HTTP Agent subclass that connects to - * the specified "HTTP(s) proxy server" in order to proxy HTTPS requests. - * - * Outgoing HTTP requests are first tunneled through the proxy server using the - * `CONNECT` HTTP request method to establish a connection to the proxy server, - * and then the proxy server connects to the destination target and issues the - * HTTP request from the proxy server. - * - * `https:` requests have their socket connection upgraded to TLS once - * the connection to the proxy server has been established. - */ -export class HttpsProxyAgent extends Agent { - static protocols = ['http', 'https'] as const; - - readonly proxy: URL; - proxyHeaders: OutgoingHttpHeaders | (() => OutgoingHttpHeaders); - connectOpts: net.TcpNetConnectOpts & tls.ConnectionOptions; - - constructor(proxy: Uri | URL, opts?: HttpsProxyAgentOptions) { - super(opts); - this.options = {}; - this.proxy = typeof proxy === 'string' ? new URL(proxy) : proxy; - this.proxyHeaders = opts?.headers ?? {}; - debug('Creating new HttpsProxyAgent instance: %o', this.proxy.href); - - // Trim off the brackets from IPv6 addresses - const host = (this.proxy.hostname || this.proxy.host).replace(/^\[|\]$/g, ''); - const port = this.proxy.port ? parseInt(this.proxy.port, 10) : this.proxy.protocol === 'https:' ? 443 : 80; - this.connectOpts = { - // Attempt to negotiate http/1.1 for proxy servers that support http/2 - ALPNProtocols: ['http/1.1'], - ...(opts ? omit(opts, 'headers') : null), - host, - port, - }; - } - - /** - * Called when the node-core HTTP client library is creating a - * new HTTP request. - */ - async connect(req: http.ClientRequest, opts: AgentConnectOpts): Promise { - const { proxy } = this; - - if (!opts.host) { - throw new TypeError('No "host" provided'); - } - - // Create a socket connection to the proxy server. - let socket: net.Socket; - if (proxy.protocol === 'https:') { - debug('Creating `tls.Socket`: %o', this.connectOpts); - const servername = this.connectOpts.servername || this.connectOpts.host; - socket = tls.connect({ - ...this.connectOpts, - servername: servername && net.isIP(servername) ? undefined : servername, - }); - } else { - debug('Creating `net.Socket`: %o', this.connectOpts); - socket = net.connect(this.connectOpts); - } - - const headers: OutgoingHttpHeaders = - typeof this.proxyHeaders === 'function' ? this.proxyHeaders() : { ...this.proxyHeaders }; - const host = net.isIPv6(opts.host) ? `[${opts.host}]` : opts.host; - let payload = `CONNECT ${host}:${opts.port} HTTP/1.1\r\n`; - - // Inject the `Proxy-Authorization` header if necessary. - if (proxy.username || proxy.password) { - const auth = `${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password)}`; - headers['Proxy-Authorization'] = `Basic ${Buffer.from(auth).toString('base64')}`; - } - - headers.Host = `${host}:${opts.port}`; - - if (!headers['Proxy-Connection']) { - headers['Proxy-Connection'] = this.keepAlive ? 'Keep-Alive' : 'close'; - } - for (const name of Object.keys(headers)) { - payload += `${name}: ${headers[name]}\r\n`; - } - - const proxyResponsePromise = parseProxyResponse(socket); - - socket.write(`${payload}\r\n`); - - const { connect, buffered } = await proxyResponsePromise; - req.emit('proxyConnect', connect); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore Not EventEmitter in Node types - this.emit('proxyConnect', connect, req); - - if (connect.statusCode === 200) { - req.once('socket', resume); - - if (opts.secureEndpoint) { - // The proxy is connecting to a TLS server, so upgrade - // this socket connection to a TLS connection. - debug('Upgrading socket connection to TLS'); - const servername = opts.servername || opts.host; - return tls.connect({ - ...omit(opts, 'host', 'path', 'port'), - socket, - servername: net.isIP(servername) ? undefined : servername, - }); - } - - return socket; - } - - // Some other status code that's not 200... need to re-play the HTTP - // header "data" events onto the socket once the HTTP machinery is - // attached so that the node core `http` can parse and handle the - // error status code. - - // Close the original socket, and a new "fake" socket is returned - // instead, so that the proxy doesn't get the HTTP request - // written to it (which may contain `Authorization` headers or other - // sensitive data). - // - // See: https://hackerone.com/reports/541502 - socket.destroy(); - - const fakeSocket = new net.Socket({ writable: false }); - fakeSocket.readable = true; - - // Need to wait for the "socket" event to re-play the "data" events. - req.once('socket', (s: net.Socket) => { - debug('Replaying proxy buffer for failed request'); - // Replay the "buffered" Buffer onto the fake `socket`, since at - // this point the HTTP module machinery has been hooked up for - // the user. - s.push(buffered); - s.push(null); - }); - - return fakeSocket; - } -} - -function resume(socket: net.Socket | tls.TLSSocket): void { - socket.resume(); -} - -function omit( - obj: T, - ...keys: K -): { - [K2 in Exclude]: T[K2]; -} { - const ret = {} as { - [K in keyof typeof obj]: (typeof obj)[K]; - }; - let key: keyof typeof obj; - for (key in obj) { - if (!keys.includes(key)) { - ret[key] = obj[key]; - } - } - return ret; -} diff --git a/packages/node-experimental/src/proxy/parse-proxy-response.ts b/packages/node-experimental/src/proxy/parse-proxy-response.ts deleted file mode 100644 index e351945e3c0f..000000000000 --- a/packages/node-experimental/src/proxy/parse-proxy-response.ts +++ /dev/null @@ -1,137 +0,0 @@ -/** - * This code was originally forked from https://github.com/TooTallNate/proxy-agents/tree/b133295fd16f6475578b6b15bd9b4e33ecb0d0b7 - * With the following licence: - * - * (The MIT License) - * - * Copyright (c) 2013 Nathan Rajlich * - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * 'Software'), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions:* - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software.* - * - * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable jsdoc/require-jsdoc */ -import type { IncomingHttpHeaders } from 'http'; -import type { Readable } from 'stream'; -import { logger } from '@sentry/utils'; - -function debug(...args: unknown[]): void { - logger.log('[https-proxy-agent:parse-proxy-response]', ...args); -} - -export interface ConnectResponse { - statusCode: number; - statusText: string; - headers: IncomingHttpHeaders; -} - -export function parseProxyResponse(socket: Readable): Promise<{ connect: ConnectResponse; buffered: Buffer }> { - return new Promise((resolve, reject) => { - // we need to buffer any HTTP traffic that happens with the proxy before we get - // the CONNECT response, so that if the response is anything other than an "200" - // response code, then we can re-play the "data" events on the socket once the - // HTTP parser is hooked up... - let buffersLength = 0; - const buffers: Buffer[] = []; - - function read() { - const b = socket.read(); - if (b) ondata(b); - else socket.once('readable', read); - } - - function cleanup() { - socket.removeListener('end', onend); - socket.removeListener('error', onerror); - socket.removeListener('readable', read); - } - - function onend() { - cleanup(); - debug('onend'); - reject(new Error('Proxy connection ended before receiving CONNECT response')); - } - - function onerror(err: Error) { - cleanup(); - debug('onerror %o', err); - reject(err); - } - - function ondata(b: Buffer) { - buffers.push(b); - buffersLength += b.length; - - const buffered = Buffer.concat(buffers, buffersLength); - const endOfHeaders = buffered.indexOf('\r\n\r\n'); - - if (endOfHeaders === -1) { - // keep buffering - debug('have not received end of HTTP headers yet...'); - read(); - return; - } - - const headerParts = buffered.slice(0, endOfHeaders).toString('ascii').split('\r\n'); - const firstLine = headerParts.shift(); - if (!firstLine) { - socket.destroy(); - return reject(new Error('No header received from proxy CONNECT response')); - } - const firstLineParts = firstLine.split(' '); - const statusCode = +firstLineParts[1]; - const statusText = firstLineParts.slice(2).join(' '); - const headers: IncomingHttpHeaders = {}; - for (const header of headerParts) { - if (!header) continue; - const firstColon = header.indexOf(':'); - if (firstColon === -1) { - socket.destroy(); - return reject(new Error(`Invalid header from proxy CONNECT response: "${header}"`)); - } - const key = header.slice(0, firstColon).toLowerCase(); - const value = header.slice(firstColon + 1).trimStart(); - const current = headers[key]; - if (typeof current === 'string') { - headers[key] = [current, value]; - } else if (Array.isArray(current)) { - current.push(value); - } else { - headers[key] = value; - } - } - debug('got proxy server response: %o %o', firstLine, headers); - cleanup(); - resolve({ - connect: { - statusCode, - statusText, - headers, - }, - buffered, - }); - } - - socket.on('error', onerror); - socket.on('end', onend); - - read(); - }); -} diff --git a/packages/node-experimental/src/transports/http-module.ts b/packages/node-experimental/src/transports/http-module.ts deleted file mode 100644 index f5cbe6fd35f9..000000000000 --- a/packages/node-experimental/src/transports/http-module.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { ClientRequest, IncomingHttpHeaders, RequestOptions as HTTPRequestOptions } from 'node:http'; -import type { RequestOptions as HTTPSRequestOptions } from 'node:https'; - -export type HTTPModuleRequestOptions = HTTPRequestOptions | HTTPSRequestOptions | string | URL; - -/** - * Cut version of http.IncomingMessage. - * Some transports work in a special Javascript environment where http.IncomingMessage is not available. - */ -export interface HTTPModuleRequestIncomingMessage { - headers: IncomingHttpHeaders; - statusCode?: number; - on(event: 'data' | 'end', listener: () => void): void; - setEncoding(encoding: string): void; -} - -/** - * Internal used interface for typescript. - * @hidden - */ -export interface HTTPModule { - /** - * Request wrapper - * @param options These are {@see TransportOptions} - * @param callback Callback when request is finished - */ - request(options: HTTPModuleRequestOptions, callback?: (res: HTTPModuleRequestIncomingMessage) => void): ClientRequest; -} diff --git a/packages/node-experimental/src/transports/http.ts b/packages/node-experimental/src/transports/http.ts deleted file mode 100644 index 4cbe7ece1f60..000000000000 --- a/packages/node-experimental/src/transports/http.ts +++ /dev/null @@ -1,177 +0,0 @@ -import * as http from 'node:http'; -import * as https from 'node:https'; -import { Readable } from 'stream'; -import { createGzip } from 'zlib'; -import { context } from '@opentelemetry/api'; -import { suppressTracing } from '@opentelemetry/core'; -import { createTransport } from '@sentry/core'; -import type { - BaseTransportOptions, - Transport, - TransportMakeRequestResponse, - TransportRequest, - TransportRequestExecutor, -} from '@sentry/types'; -import { consoleSandbox } from '@sentry/utils'; -import { HttpsProxyAgent } from '../proxy'; -import type { HTTPModule } from './http-module'; - -export interface NodeTransportOptions extends BaseTransportOptions { - /** Define custom headers */ - headers?: Record; - /** Set a proxy that should be used for outbound requests. */ - proxy?: string; - /** HTTPS proxy CA certificates */ - caCerts?: string | Buffer | Array; - /** Custom HTTP module. Defaults to the native 'http' and 'https' modules. */ - httpModule?: HTTPModule; - /** Allow overriding connection keepAlive, defaults to false */ - keepAlive?: boolean; -} - -// Estimated maximum size for reasonable standalone event -const GZIP_THRESHOLD = 1024 * 32; - -/** - * Gets a stream from a Uint8Array or string - * Readable.from is ideal but was added in node.js v12.3.0 and v10.17.0 - */ -function streamFromBody(body: Uint8Array | string): Readable { - return new Readable({ - read() { - this.push(body); - this.push(null); - }, - }); -} - -/** - * Creates a Transport that uses native the native 'http' and 'https' modules to send events to Sentry. - */ -export function makeNodeTransport(options: NodeTransportOptions): Transport { - let urlSegments: URL; - - try { - urlSegments = new URL(options.url); - } catch (e) { - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.warn( - '[@sentry/node]: Invalid dsn or tunnel option, will not send any events. The tunnel option must be a full URL when used.', - ); - }); - return createTransport(options, () => Promise.resolve({})); - } - - const isHttps = urlSegments.protocol === 'https:'; - - // Proxy prioritization: http => `options.proxy` | `process.env.http_proxy` - // Proxy prioritization: https => `options.proxy` | `process.env.https_proxy` | `process.env.http_proxy` - const proxy = applyNoProxyOption( - urlSegments, - options.proxy || (isHttps ? process.env.https_proxy : undefined) || process.env.http_proxy, - ); - - const nativeHttpModule = isHttps ? https : http; - const keepAlive = options.keepAlive === undefined ? false : options.keepAlive; - - // TODO(v7): Evaluate if we can set keepAlive to true. This would involve testing for memory leaks in older node - // versions(>= 8) as they had memory leaks when using it: #2555 - const agent = proxy - ? (new HttpsProxyAgent(proxy) as http.Agent) - : new nativeHttpModule.Agent({ keepAlive, maxSockets: 30, timeout: 2000 }); - - // This ensures we do not generate any spans in OpenTelemetry for the transport - return context.with(suppressTracing(context.active()), () => { - const requestExecutor = createRequestExecutor(options, options.httpModule ?? nativeHttpModule, agent); - return createTransport(options, requestExecutor); - }); -} - -/** - * Honors the `no_proxy` env variable with the highest priority to allow for hosts exclusion. - * - * @param transportUrl The URL the transport intends to send events to. - * @param proxy The client configured proxy. - * @returns A proxy the transport should use. - */ -function applyNoProxyOption(transportUrlSegments: URL, proxy: string | undefined): string | undefined { - const { no_proxy } = process.env; - - const urlIsExemptFromProxy = - no_proxy && - no_proxy - .split(',') - .some( - exemption => transportUrlSegments.host.endsWith(exemption) || transportUrlSegments.hostname.endsWith(exemption), - ); - - if (urlIsExemptFromProxy) { - return undefined; - } else { - return proxy; - } -} - -/** - * Creates a RequestExecutor to be used with `createTransport`. - */ -function createRequestExecutor( - options: NodeTransportOptions, - httpModule: HTTPModule, - agent: http.Agent, -): TransportRequestExecutor { - const { hostname, pathname, port, protocol, search } = new URL(options.url); - return function makeRequest(request: TransportRequest): Promise { - return new Promise((resolve, reject) => { - let body = streamFromBody(request.body); - - const headers: Record = { ...options.headers }; - - if (request.body.length > GZIP_THRESHOLD) { - headers['content-encoding'] = 'gzip'; - body = body.pipe(createGzip()); - } - - const req = httpModule.request( - { - method: 'POST', - agent, - headers, - hostname, - path: `${pathname}${search}`, - port, - protocol, - ca: options.caCerts, - }, - res => { - res.on('data', () => { - // Drain socket - }); - - res.on('end', () => { - // Drain socket - }); - - res.setEncoding('utf8'); - - // "Key-value pairs of header names and values. Header names are lower-cased." - // https://nodejs.org/api/http.html#http_message_headers - const retryAfterHeader = res.headers['retry-after'] ?? null; - const rateLimitsHeader = res.headers['x-sentry-rate-limits'] ?? null; - - resolve({ - statusCode: res.statusCode, - headers: { - 'retry-after': retryAfterHeader, - 'x-sentry-rate-limits': Array.isArray(rateLimitsHeader) ? rateLimitsHeader[0] : rateLimitsHeader, - }, - }); - }, - ); - - req.on('error', reject); - body.pipe(req); - }); - }; -} diff --git a/packages/node-experimental/src/transports/index.ts b/packages/node-experimental/src/transports/index.ts deleted file mode 100644 index ba59ba8878a4..000000000000 --- a/packages/node-experimental/src/transports/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type { NodeTransportOptions } from './http'; - -export { makeNodeTransport } from './http'; diff --git a/packages/node-experimental/src/types.ts b/packages/node-experimental/src/types.ts deleted file mode 100644 index d78e1761fd79..000000000000 --- a/packages/node-experimental/src/types.ts +++ /dev/null @@ -1,107 +0,0 @@ -import type { Span as WriteableSpan } from '@opentelemetry/api'; -import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'; -import type { ClientOptions, Options, SamplingContext, Scope, Span, TracePropagationTargets } from '@sentry/types'; - -import type { NodeTransportOptions } from './transports'; - -export interface BaseNodeOptions { - /** - * List of strings/regex controlling to which outgoing requests - * the SDK will attach tracing headers. - * - * By default the SDK will attach those headers to all outgoing - * requests. If this option is provided, the SDK will match the - * request URL of outgoing requests against the items in this - * array, and only attach tracing headers if a match was found. - * - * @example - * ```js - * Sentry.init({ - * tracePropagationTargets: ['api.site.com'], - * }); - * ``` - */ - tracePropagationTargets?: TracePropagationTargets; - - /** - * Sets profiling sample rate when @sentry/profiling-node is installed - */ - profilesSampleRate?: number; - - /** - * Function to compute profiling sample rate dynamically and filter unwanted profiles. - * - * Profiling is enabled if either this or `profilesSampleRate` is defined. If both are defined, `profilesSampleRate` is - * ignored. - * - * Will automatically be passed a context object of default and optional custom data. See - * {@link Transaction.samplingContext} and {@link Hub.startTransaction}. - * - * @returns A sample rate between 0 and 1 (0 drops the profile, 1 guarantees it will be sent). Returning `true` is - * equivalent to returning 1 and returning `false` is equivalent to returning 0. - */ - profilesSampler?: (samplingContext: SamplingContext) => number | boolean; - - /** Sets an optional server name (device name) */ - serverName?: string; - - /** - * Include local variables with stack traces. - * - * Requires the `LocalVariables` integration. - */ - includeLocalVariables?: boolean; - - /** - * If you use Spotlight by Sentry during development, use - * this option to forward captured Sentry events to Spotlight. - * - * Either set it to true, or provide a specific Spotlight Sidecar URL. - * - * More details: https://spotlightjs.com/ - * - * IMPORTANT: Only set this option to `true` while developing, not in production! - */ - spotlight?: boolean | string; - - /** - * If this is set to true, the SDK will not set up OpenTelemetry automatically. - * In this case, you _have_ to ensure to set it up correctly yourself, including: - * * The `SentrySpanProcessor` - * * The `SentryPropagator` - * * The `SentryContextManager` - * * The `SentrySampler` - */ - skipOpenTelemetrySetup?: boolean; - - /** Callback that is executed when a fatal global error occurs. */ - onFatalError?(this: void, error: Error): void; -} - -/** - * Configuration options for the Sentry Node SDK - * @see @sentry/types Options for more information. - */ -export interface NodeOptions extends Options, BaseNodeOptions {} - -/** - * Configuration options for the Sentry Node SDK Client class - * @see NodeClient for more information. - */ -export interface NodeClientOptions extends ClientOptions, BaseNodeOptions {} - -export interface CurrentScopes { - scope: Scope; - isolationScope: Scope; -} - -/** - * The base `Span` type is basically a `WriteableSpan`. - * There are places where we basically want to allow passing _any_ span, - * so in these cases we type this as `AbstractSpan` which could be either a regular `Span` or a `ReadableSpan`. - * You'll have to make sur to check revelant fields before accessing them. - * - * Note that technically, the `Span` exported from `@opentelemwetry/sdk-trace-base` matches this, - * but we cannot be 100% sure that we are actually getting such a span, so this type is more defensive. - */ -export type AbstractSpan = WriteableSpan | ReadableSpan | Span; diff --git a/packages/node-experimental/test/cron.test.ts b/packages/node-experimental/test/cron.test.ts deleted file mode 100644 index d068280a41e0..000000000000 --- a/packages/node-experimental/test/cron.test.ts +++ /dev/null @@ -1,220 +0,0 @@ -import * as SentryCore from '@sentry/core'; - -import { cron } from '../src'; -import type { CronJob, CronJobParams } from '../src/cron/cron'; -import type { NodeCron, NodeCronOptions } from '../src/cron/node-cron'; - -describe('cron check-ins', () => { - let withMonitorSpy: jest.SpyInstance; - - beforeEach(() => { - withMonitorSpy = jest.spyOn(SentryCore, 'withMonitor'); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - describe('cron', () => { - class CronJobMock { - constructor( - cronTime: CronJobParams['cronTime'], - onTick: CronJobParams['onTick'], - _onComplete?: CronJobParams['onComplete'], - _start?: CronJobParams['start'], - _timeZone?: CronJobParams['timeZone'], - _context?: CronJobParams['context'], - _runOnInit?: CronJobParams['runOnInit'], - _utcOffset?: CronJobParams['utcOffset'], - _unrefTimeout?: CronJobParams['unrefTimeout'], - ) { - expect(cronTime).toBe('* * * Jan,Sep Sun'); - expect(onTick).toBeInstanceOf(Function); - setImmediate(() => onTick(undefined, undefined)); - } - - static from(params: CronJobParams): CronJob { - return new CronJobMock( - params.cronTime, - params.onTick, - params.onComplete, - params.start, - params.timeZone, - params.context, - params.runOnInit, - params.utcOffset, - params.unrefTimeout, - ); - } - } - - test('new CronJob()', done => { - expect.assertions(4); - - const CronJobWithCheckIn = cron.instrumentCron(CronJobMock, 'my-cron-job'); - - new CronJobWithCheckIn( - '* * * Jan,Sep Sun', - () => { - expect(withMonitorSpy).toHaveBeenCalledTimes(1); - expect(withMonitorSpy).toHaveBeenLastCalledWith('my-cron-job', expect.anything(), { - schedule: { type: 'crontab', value: '* * * 1,9 0' }, - timezone: 'America/Los_Angeles', - }); - done(); - }, - undefined, - true, - 'America/Los_Angeles', - ); - }); - - test('CronJob.from()', done => { - expect.assertions(4); - - const CronJobWithCheckIn = cron.instrumentCron(CronJobMock, 'my-cron-job'); - - CronJobWithCheckIn.from({ - cronTime: '* * * Jan,Sep Sun', - onTick: () => { - expect(withMonitorSpy).toHaveBeenCalledTimes(1); - expect(withMonitorSpy).toHaveBeenLastCalledWith('my-cron-job', expect.anything(), { - schedule: { type: 'crontab', value: '* * * 1,9 0' }, - }); - done(); - }, - }); - }); - - test('throws with multiple jobs same name', () => { - const CronJobWithCheckIn = cron.instrumentCron(CronJobMock, 'my-cron-job'); - - CronJobWithCheckIn.from({ - cronTime: '* * * Jan,Sep Sun', - onTick: () => { - // - }, - }); - - expect(() => { - CronJobWithCheckIn.from({ - cronTime: '* * * Jan,Sep Sun', - onTick: () => { - // - }, - }); - }).toThrowError("A job named 'my-cron-job' has already been scheduled"); - }); - }); - - describe('node-cron', () => { - test('calls withMonitor', done => { - expect.assertions(5); - - const nodeCron: NodeCron = { - schedule: (expression: string, callback: () => void, options?: NodeCronOptions): unknown => { - expect(expression).toBe('* * * Jan,Sep Sun'); - expect(callback).toBeInstanceOf(Function); - expect(options?.name).toBe('my-cron-job'); - return callback(); - }, - }; - - const cronWithCheckIn = cron.instrumentNodeCron(nodeCron); - - cronWithCheckIn.schedule( - '* * * Jan,Sep Sun', - () => { - expect(withMonitorSpy).toHaveBeenCalledTimes(1); - expect(withMonitorSpy).toHaveBeenLastCalledWith('my-cron-job', expect.anything(), { - schedule: { type: 'crontab', value: '* * * 1,9 0' }, - }); - done(); - }, - { name: 'my-cron-job' }, - ); - }); - - test('throws without supplied name', () => { - const nodeCron: NodeCron = { - schedule: (): unknown => { - return undefined; - }, - }; - - const cronWithCheckIn = cron.instrumentNodeCron(nodeCron); - - expect(() => { - // @ts-expect-error Initially missing name - cronWithCheckIn.schedule('* * * * *', () => { - // - }); - }).toThrowError('Missing "name" for scheduled job. A name is required for Sentry check-in monitoring.'); - }); - }); - - describe('node-schedule', () => { - test('calls withMonitor', done => { - expect.assertions(5); - - class NodeScheduleMock { - scheduleJob( - nameOrExpression: string | Date | object, - expressionOrCallback: string | Date | object | (() => void), - callback: () => void, - ): unknown { - expect(nameOrExpression).toBe('my-cron-job'); - expect(expressionOrCallback).toBe('* * * Jan,Sep Sun'); - expect(callback).toBeInstanceOf(Function); - return callback(); - } - } - - const scheduleWithCheckIn = cron.instrumentNodeSchedule(new NodeScheduleMock()); - - scheduleWithCheckIn.scheduleJob('my-cron-job', '* * * Jan,Sep Sun', () => { - expect(withMonitorSpy).toHaveBeenCalledTimes(1); - expect(withMonitorSpy).toHaveBeenLastCalledWith('my-cron-job', expect.anything(), { - schedule: { type: 'crontab', value: '* * * 1,9 0' }, - }); - done(); - }); - }); - - test('throws without crontab string', () => { - class NodeScheduleMock { - scheduleJob(_: string, __: string | Date, ___: () => void): unknown { - return undefined; - } - } - - const scheduleWithCheckIn = cron.instrumentNodeSchedule(new NodeScheduleMock()); - - expect(() => { - scheduleWithCheckIn.scheduleJob('my-cron-job', new Date(), () => { - // - }); - }).toThrowError( - "Automatic instrumentation of 'node-schedule' requires the first parameter of 'scheduleJob' to be a job name string and the second parameter to be a crontab string", - ); - }); - - test('throws without job name', () => { - class NodeScheduleMock { - scheduleJob(_: string, __: () => void): unknown { - return undefined; - } - } - - const scheduleWithCheckIn = cron.instrumentNodeSchedule(new NodeScheduleMock()); - - expect(() => { - scheduleWithCheckIn.scheduleJob('* * * * *', () => { - // - }); - }).toThrowError( - "Automatic instrumentation of 'node-schedule' requires the first parameter of 'scheduleJob' to be a job name string and the second parameter to be a crontab string", - ); - }); - }); -}); diff --git a/packages/node-experimental/test/integrations/context.test.ts b/packages/node-experimental/test/integrations/context.test.ts deleted file mode 100644 index 519e101187ff..000000000000 --- a/packages/node-experimental/test/integrations/context.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as os from 'os'; - -import { getDeviceContext } from '../../src/integrations/context'; - -describe('Context', () => { - describe('getDeviceContext', () => { - afterAll(() => { - jest.clearAllMocks(); - }); - - it('returns boot time if os.uptime is defined and returns a valid uptime', () => { - const deviceCtx = getDeviceContext({}); - expect(deviceCtx.boot_time).toEqual(expect.any(String)); - }); - - it('returns no boot time if os.uptime() returns undefined', () => { - jest.spyOn(os, 'uptime').mockReturnValue(undefined as unknown as number); - const deviceCtx = getDeviceContext({}); - expect(deviceCtx.boot_time).toBeUndefined(); - }); - }); -}); diff --git a/packages/node-experimental/test/integrations/contextlines.test.ts b/packages/node-experimental/test/integrations/contextlines.test.ts deleted file mode 100644 index c4ef1efaa292..000000000000 --- a/packages/node-experimental/test/integrations/contextlines.test.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { promises } from 'fs'; -import type { StackFrame } from '@sentry/types'; -import { parseStackFrames } from '@sentry/utils'; - -import { _contextLinesIntegration, resetFileContentCache } from '../../src/integrations/contextlines'; -import { defaultStackParser } from '../../src/sdk/api'; -import { getError } from '../helpers/error'; - -jest.mock('fs', () => { - const actual = jest.requireActual('fs'); - return { - ...actual, - promises: { - ...actual.promises, - readFile: jest.fn(actual.promises), - }, - }; -}); - -describe('ContextLines', () => { - const readFileSpy = promises.readFile as unknown as jest.SpyInstance; - let contextLines: ReturnType; - - async function addContext(frames: StackFrame[]): Promise { - await contextLines.processEvent({ exception: { values: [{ stacktrace: { frames } }] } }); - } - - beforeEach(() => { - contextLines = _contextLinesIntegration(); - resetFileContentCache(); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('lru file cache', () => { - test('parseStack with same file', async () => { - expect.assertions(1); - - const frames = parseStackFrames(defaultStackParser, new Error('test')); - - await addContext(Array.from(frames)); - - const numCalls = readFileSpy.mock.calls.length; - await addContext(frames); - - // Calls to `readFile` shouldn't increase if there isn't a new error to - // parse whose stacktrace contains a file we haven't yet seen - expect(readFileSpy).toHaveBeenCalledTimes(numCalls); - }); - - test('parseStack with ESM module names', async () => { - expect.assertions(1); - - const framesWithFilePath: StackFrame[] = [ - { - colno: 1, - filename: 'file:///var/task/index.js', - lineno: 1, - function: 'fxn1', - }, - ]; - - await addContext(framesWithFilePath); - expect(readFileSpy).toHaveBeenCalledTimes(1); - }); - - test('parseStack with adding different file', async () => { - expect.assertions(1); - const frames = parseStackFrames(defaultStackParser, new Error('test')); - - await addContext(frames); - - const numCalls = readFileSpy.mock.calls.length; - const parsedFrames = parseStackFrames(defaultStackParser, getError()); - await addContext(parsedFrames); - - const newErrorCalls = readFileSpy.mock.calls.length; - expect(newErrorCalls).toBeGreaterThan(numCalls); - }); - - test('parseStack with duplicate files', async () => { - expect.assertions(1); - const framesWithDuplicateFiles: StackFrame[] = [ - { - colno: 1, - filename: '/var/task/index.js', - lineno: 1, - function: 'fxn1', - }, - { - colno: 2, - filename: '/var/task/index.js', - lineno: 2, - function: 'fxn2', - }, - { - colno: 3, - filename: '/var/task/index.js', - lineno: 3, - function: 'fxn3', - }, - ]; - - await addContext(framesWithDuplicateFiles); - expect(readFileSpy).toHaveBeenCalledTimes(1); - }); - - test('parseStack with no context', async () => { - contextLines = _contextLinesIntegration({ frameContextLines: 0 }); - - expect.assertions(1); - const frames = parseStackFrames(defaultStackParser, new Error('test')); - - await addContext(frames); - expect(readFileSpy).toHaveBeenCalledTimes(0); - }); - }); - - test('does not attempt to readfile multiple times if it fails', async () => { - expect.assertions(1); - - readFileSpy.mockImplementation(() => { - throw new Error("ENOENT: no such file or directory, open '/does/not/exist.js'"); - }); - - await addContext([ - { - colno: 1, - filename: '/does/not/exist.js', - lineno: 1, - function: 'fxn1', - }, - ]); - await addContext([ - { - colno: 1, - filename: '/does/not/exist.js', - lineno: 1, - function: 'fxn1', - }, - ]); - - expect(readFileSpy).toHaveBeenCalledTimes(1); - }); -}); diff --git a/packages/node-experimental/test/integrations/localvariables.test.ts b/packages/node-experimental/test/integrations/localvariables.test.ts deleted file mode 100644 index db9385214d42..000000000000 --- a/packages/node-experimental/test/integrations/localvariables.test.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { createRateLimiter } from '../../src/integrations/local-variables/common'; -import { createCallbackList } from '../../src/integrations/local-variables/local-variables-sync'; -import { NODE_MAJOR } from '../../src/nodeVersion'; - -jest.setTimeout(20_000); - -const describeIf = (condition: boolean) => (condition ? describe : describe.skip); - -describeIf(NODE_MAJOR >= 18)('LocalVariables', () => { - describe('createCallbackList', () => { - it('Should call callbacks in reverse order', done => { - const log: number[] = []; - - const { add, next } = createCallbackList(n => { - expect(log).toEqual([5, 4, 3, 2, 1]); - expect(n).toBe(15); - done(); - }); - - add(n => { - log.push(1); - next(n + 1); - }); - - add(n => { - log.push(2); - next(n + 1); - }); - - add(n => { - log.push(3); - next(n + 1); - }); - - add(n => { - log.push(4); - next(n + 1); - }); - - add(n => { - log.push(5); - next(n + 11); - }); - - next(0); - }); - - it('only calls complete once even if multiple next', done => { - const { add, next } = createCallbackList(n => { - expect(n).toBe(1); - done(); - }); - - add(n => { - next(n + 1); - // We dont actually do this in our code... - next(n + 1); - }); - - next(0); - }); - - it('calls completed if added closure throws', done => { - const { add, next } = createCallbackList(n => { - expect(n).toBe(10); - done(); - }); - - add(n => { - throw new Error('test'); - next(n + 1); - }); - - next(10); - }); - }); - - describe('rateLimiter', () => { - it('calls disable if exceeded', done => { - const increment = createRateLimiter( - 5, - () => {}, - () => { - done(); - }, - ); - - for (let i = 0; i < 7; i++) { - increment(); - } - }); - - it('does not call disable if not exceeded', done => { - const increment = createRateLimiter( - 5, - () => { - throw new Error('Should not be called'); - }, - () => { - throw new Error('Should not be called'); - }, - ); - - let count = 0; - - const timer = setInterval(() => { - for (let i = 0; i < 4; i++) { - increment(); - } - - count += 1; - - if (count >= 5) { - clearInterval(timer); - done(); - } - }, 1_000); - }); - - it('re-enables after timeout', done => { - let called = false; - - const increment = createRateLimiter( - 5, - () => { - expect(called).toEqual(true); - done(); - }, - () => { - expect(called).toEqual(false); - called = true; - }, - ); - - for (let i = 0; i < 10; i++) { - increment(); - } - }); - }); -}); diff --git a/packages/node-experimental/test/integrations/spotlight.test.ts b/packages/node-experimental/test/integrations/spotlight.test.ts deleted file mode 100644 index 6b888c22edcd..000000000000 --- a/packages/node-experimental/test/integrations/spotlight.test.ts +++ /dev/null @@ -1,181 +0,0 @@ -import * as http from 'http'; -import type { Envelope, EventEnvelope } from '@sentry/types'; -import { createEnvelope, logger } from '@sentry/utils'; - -import { spotlightIntegration } from '../../src/integrations/spotlight'; -import { NodeClient } from '../../src/sdk/client'; -import { getDefaultNodeClientOptions } from '../helpers/getDefaultNodeClientOptions'; - -describe('Spotlight', () => { - const loggerSpy = jest.spyOn(logger, 'warn'); - - afterEach(() => { - loggerSpy.mockClear(); - jest.clearAllMocks(); - }); - - const options = getDefaultNodeClientOptions(); - const client = new NodeClient(options); - - it('has a name', () => { - const integration = spotlightIntegration(); - expect(integration.name).toEqual('Spotlight'); - }); - - it('registers a callback on the `beforeEnvelope` hook', () => { - const clientWithSpy = { - ...client, - on: jest.fn(), - }; - const integration = spotlightIntegration(); - // @ts-expect-error - this is fine in tests - integration.setup(clientWithSpy); - expect(clientWithSpy.on).toHaveBeenCalledWith('beforeEnvelope', expect.any(Function)); - }); - - it('sends an envelope POST request to the sidecar url', () => { - const httpSpy = jest.spyOn(http, 'request').mockImplementationOnce(() => { - return { - on: jest.fn(), - write: jest.fn(), - end: jest.fn(), - } as any; - }); - - let callback: (envelope: Envelope) => void = () => {}; - const clientWithSpy = { - ...client, - on: jest.fn().mockImplementationOnce((_, cb) => (callback = cb)), - }; - - const integration = spotlightIntegration(); - // @ts-expect-error - this is fine in tests - integration.setup(clientWithSpy); - - const envelope = createEnvelope({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [ - [{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }], - ]); - - callback(envelope); - - expect(httpSpy).toHaveBeenCalledWith( - { - headers: { - 'Content-Type': 'application/x-sentry-envelope', - }, - hostname: 'localhost', - method: 'POST', - path: '/stream', - port: '8969', - }, - expect.any(Function), - ); - }); - - it('sends an envelope POST request to a custom sidecar url', () => { - const httpSpy = jest.spyOn(http, 'request').mockImplementationOnce(() => { - return { - on: jest.fn(), - write: jest.fn(), - end: jest.fn(), - } as any; - }); - - let callback: (envelope: Envelope) => void = () => {}; - const clientWithSpy = { - ...client, - on: jest.fn().mockImplementationOnce((_, cb) => (callback = cb)), - }; - - const integration = spotlightIntegration({ sidecarUrl: 'http://mylocalhost:8888/abcd' }); - // @ts-expect-error - this is fine in tests - integration.setup(clientWithSpy); - - const envelope = createEnvelope({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [ - [{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }], - ]); - - callback(envelope); - - expect(httpSpy).toHaveBeenCalledWith( - { - headers: { - 'Content-Type': 'application/x-sentry-envelope', - }, - hostname: 'mylocalhost', - method: 'POST', - path: '/abcd', - port: '8888', - }, - expect.any(Function), - ); - }); - - describe('no-ops if', () => { - it('an invalid URL is passed', () => { - const integration = spotlightIntegration({ sidecarUrl: 'invalid-url' }); - integration.setup!(client); - expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid sidecar URL: invalid-url')); - }); - }); - - it('warns if the NODE_ENV variable doesn\'t equal "development"', () => { - const oldEnvValue = process.env.NODE_ENV; - process.env.NODE_ENV = 'production'; - - const integration = spotlightIntegration({ sidecarUrl: 'http://localhost:8969' }); - integration.setup!(client); - - expect(loggerSpy).toHaveBeenCalledWith( - expect.stringContaining("It seems you're not in dev mode. Do you really want to have Spotlight enabled?"), - ); - - process.env.NODE_ENV = oldEnvValue; - }); - - it('doesn\'t warn if the NODE_ENV variable equals "development"', () => { - const oldEnvValue = process.env.NODE_ENV; - process.env.NODE_ENV = 'development'; - - const integration = spotlightIntegration({ sidecarUrl: 'http://localhost:8969' }); - integration.setup!(client); - - expect(loggerSpy).not.toHaveBeenCalledWith( - expect.stringContaining("It seems you're not in dev mode. Do you really want to have Spotlight enabled?"), - ); - - process.env.NODE_ENV = oldEnvValue; - }); - - it('handles `process` not being available', () => { - const originalProcess = process; - - // @ts-expect-error - TS complains but we explicitly wanna test this - delete global.process; - - const integration = spotlightIntegration({ sidecarUrl: 'http://localhost:8969' }); - integration.setup!(client); - - expect(loggerSpy).not.toHaveBeenCalledWith( - expect.stringContaining("It seems you're not in dev mode. Do you really want to have Spotlight enabled?"), - ); - - global.process = originalProcess; - }); - - it('handles `process.env` not being available', () => { - const originalEnv = process.env; - - // @ts-expect-error - TS complains but we explicitly wanna test this - delete process.env; - - const integration = spotlightIntegration({ sidecarUrl: 'http://localhost:8969' }); - integration.setup!(client); - - expect(loggerSpy).not.toHaveBeenCalledWith( - expect.stringContaining("It seems you're not in dev mode. Do you really want to have Spotlight enabled?"), - ); - - process.env = originalEnv; - }); -}); diff --git a/packages/node-experimental/test/transports/http.test.ts b/packages/node-experimental/test/transports/http.test.ts deleted file mode 100644 index e945c086959a..000000000000 --- a/packages/node-experimental/test/transports/http.test.ts +++ /dev/null @@ -1,411 +0,0 @@ -import * as http from 'http'; -import { createGunzip } from 'zlib'; -import { createTransport } from '@sentry/core'; -import type { EventEnvelope, EventItem } from '@sentry/types'; -import { addItemToEnvelope, createAttachmentEnvelopeItem, createEnvelope, serializeEnvelope } from '@sentry/utils'; - -import { makeNodeTransport } from '../../src/transports'; - -jest.mock('@sentry/core', () => { - const actualCore = jest.requireActual('@sentry/core'); - return { - ...actualCore, - createTransport: jest.fn().mockImplementation(actualCore.createTransport), - }; -}); - -import * as httpProxyAgent from '../../src/proxy'; - -const SUCCESS = 200; -const RATE_LIMIT = 429; -const INVALID = 400; -const FAILED = 500; - -interface TestServerOptions { - statusCode: number; - responseHeaders?: Record; -} - -let testServer: http.Server | undefined; - -function setupTestServer( - options: TestServerOptions, - requestInspector?: (req: http.IncomingMessage, body: string, raw: Uint8Array) => void, -) { - testServer = http.createServer((req, res) => { - const chunks: Buffer[] = []; - - const stream = req.headers['content-encoding'] === 'gzip' ? req.pipe(createGunzip({})) : req; - - stream.on('data', data => { - chunks.push(data); - }); - - stream.on('end', () => { - requestInspector?.(req, chunks.join(), Buffer.concat(chunks)); - }); - - res.writeHead(options.statusCode, options.responseHeaders); - res.end(); - - // also terminate socket because keepalive hangs connection a bit - // eslint-disable-next-line deprecation/deprecation - res.connection?.end(); - }); - - testServer.listen(18101); - - return new Promise(resolve => { - testServer?.on('listening', resolve); - }); -} - -const TEST_SERVER_URL = 'http://localhost:18101'; - -const EVENT_ENVELOPE = createEnvelope({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [ - [{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem, -]); - -const SERIALIZED_EVENT_ENVELOPE = serializeEnvelope(EVENT_ENVELOPE); - -const ATTACHMENT_ITEM = createAttachmentEnvelopeItem({ filename: 'empty-file.bin', data: new Uint8Array(50_000) }); -const EVENT_ATTACHMENT_ENVELOPE = addItemToEnvelope(EVENT_ENVELOPE, ATTACHMENT_ITEM); -const SERIALIZED_EVENT_ATTACHMENT_ENVELOPE = serializeEnvelope(EVENT_ATTACHMENT_ENVELOPE) as Uint8Array; - -const defaultOptions = { - url: TEST_SERVER_URL, - recordDroppedEvent: () => undefined, -}; - -// empty function to keep test output clean -const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); - -afterEach(done => { - jest.clearAllMocks(); - - if (testServer && testServer.listening) { - testServer.close(done); - } else { - done(); - } -}); - -describe('makeNewHttpTransport()', () => { - describe('.send()', () => { - it('should correctly send envelope to server', async () => { - await setupTestServer({ statusCode: SUCCESS }, (req, body) => { - expect(req.method).toBe('POST'); - expect(body).toBe(SERIALIZED_EVENT_ENVELOPE); - }); - - const transport = makeNodeTransport(defaultOptions); - await transport.send(EVENT_ENVELOPE); - }); - - it('allows overriding keepAlive', async () => { - await setupTestServer({ statusCode: SUCCESS }, req => { - expect(req.headers).toEqual( - expect.objectContaining({ - // node http module lower-cases incoming headers - connection: 'keep-alive', - }), - ); - }); - - const transport = makeNodeTransport({ keepAlive: true, ...defaultOptions }); - await transport.send(EVENT_ENVELOPE); - }); - - it('should correctly send user-provided headers to server', async () => { - await setupTestServer({ statusCode: SUCCESS }, req => { - expect(req.headers).toEqual( - expect.objectContaining({ - // node http module lower-cases incoming headers - 'x-some-custom-header-1': 'value1', - 'x-some-custom-header-2': 'value2', - }), - ); - }); - - const transport = makeNodeTransport({ - ...defaultOptions, - headers: { - 'X-Some-Custom-Header-1': 'value1', - 'X-Some-Custom-Header-2': 'value2', - }, - }); - - await transport.send(EVENT_ENVELOPE); - }); - - it.each([RATE_LIMIT, INVALID, FAILED])( - 'should resolve on bad server response (status %i)', - async serverStatusCode => { - await setupTestServer({ statusCode: serverStatusCode }); - - const transport = makeNodeTransport(defaultOptions); - - await expect(transport.send(EVENT_ENVELOPE)).resolves.toEqual( - expect.objectContaining({ statusCode: serverStatusCode }), - ); - }, - ); - - it('should resolve when server responds with rate limit header and status code 200', async () => { - await setupTestServer({ - statusCode: SUCCESS, - responseHeaders: { - 'Retry-After': '2700', - 'X-Sentry-Rate-Limits': '60::organization, 2700::organization', - }, - }); - - const transport = makeNodeTransport(defaultOptions); - await expect(transport.send(EVENT_ENVELOPE)).resolves.toEqual({ - statusCode: SUCCESS, - headers: { - 'retry-after': '2700', - 'x-sentry-rate-limits': '60::organization, 2700::organization', - }, - }); - }); - }); - - describe('compression', () => { - it('small envelopes should not be compressed', async () => { - await setupTestServer( - { - statusCode: SUCCESS, - responseHeaders: {}, - }, - (req, body) => { - expect(req.headers['content-encoding']).toBeUndefined(); - expect(body).toBe(SERIALIZED_EVENT_ENVELOPE); - }, - ); - - const transport = makeNodeTransport(defaultOptions); - await transport.send(EVENT_ENVELOPE); - }); - - it('large envelopes should be compressed', async () => { - await setupTestServer( - { - statusCode: SUCCESS, - responseHeaders: {}, - }, - (req, _, raw) => { - expect(req.headers['content-encoding']).toEqual('gzip'); - expect(raw.buffer).toStrictEqual(SERIALIZED_EVENT_ATTACHMENT_ENVELOPE.buffer); - }, - ); - - const transport = makeNodeTransport(defaultOptions); - await transport.send(EVENT_ATTACHMENT_ENVELOPE); - }); - }); - - describe('proxy', () => { - const proxyAgentSpy = jest - .spyOn(httpProxyAgent, 'HttpsProxyAgent') - // @ts-expect-error using http agent as https proxy agent - .mockImplementation(() => new http.Agent({ keepAlive: false, maxSockets: 30, timeout: 2000 })); - - it('can be configured through option', () => { - makeNodeTransport({ - ...defaultOptions, - url: 'http://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', - proxy: 'http://example.com', - }); - - expect(proxyAgentSpy).toHaveBeenCalledTimes(1); - expect(proxyAgentSpy).toHaveBeenCalledWith('http://example.com'); - }); - - it('can be configured through env variables option', () => { - process.env.http_proxy = 'http://example.com'; - makeNodeTransport({ - ...defaultOptions, - url: 'http://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', - }); - - expect(proxyAgentSpy).toHaveBeenCalledTimes(1); - expect(proxyAgentSpy).toHaveBeenCalledWith('http://example.com'); - delete process.env.http_proxy; - }); - - it('client options have priority over env variables', () => { - process.env.http_proxy = 'http://foo.com'; - makeNodeTransport({ - ...defaultOptions, - url: 'http://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', - proxy: 'http://bar.com', - }); - - expect(proxyAgentSpy).toHaveBeenCalledTimes(1); - expect(proxyAgentSpy).toHaveBeenCalledWith('http://bar.com'); - delete process.env.http_proxy; - }); - - it('no_proxy allows for skipping specific hosts', () => { - process.env.no_proxy = 'sentry.io'; - makeNodeTransport({ - ...defaultOptions, - url: 'http://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', - proxy: 'http://example.com', - }); - - expect(proxyAgentSpy).not.toHaveBeenCalled(); - - delete process.env.no_proxy; - }); - - it('no_proxy works with a port', () => { - process.env.http_proxy = 'http://example.com:8080'; - process.env.no_proxy = 'sentry.io:8989'; - - makeNodeTransport({ - ...defaultOptions, - url: 'http://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', - }); - - expect(proxyAgentSpy).not.toHaveBeenCalled(); - - delete process.env.no_proxy; - delete process.env.http_proxy; - }); - - it('no_proxy works with multiple comma-separated hosts', () => { - process.env.http_proxy = 'http://example.com:8080'; - process.env.no_proxy = 'example.com,sentry.io,wat.com:1337'; - - makeNodeTransport({ - ...defaultOptions, - url: 'http://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', - }); - - expect(proxyAgentSpy).not.toHaveBeenCalled(); - - delete process.env.no_proxy; - delete process.env.http_proxy; - }); - }); - - describe('should register TransportRequestExecutor that returns the correct object from server response', () => { - it('rate limit', async () => { - await setupTestServer({ - statusCode: RATE_LIMIT, - responseHeaders: {}, - }); - - makeNodeTransport(defaultOptions); - const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; - - const executorResult = registeredRequestExecutor({ - body: serializeEnvelope(EVENT_ENVELOPE), - category: 'error', - }); - - await expect(executorResult).resolves.toEqual( - expect.objectContaining({ - statusCode: RATE_LIMIT, - }), - ); - }); - - it('OK', async () => { - await setupTestServer({ - statusCode: SUCCESS, - }); - - makeNodeTransport(defaultOptions); - const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; - - const executorResult = registeredRequestExecutor({ - body: serializeEnvelope(EVENT_ENVELOPE), - category: 'error', - }); - - await expect(executorResult).resolves.toEqual( - expect.objectContaining({ - statusCode: SUCCESS, - headers: { - 'retry-after': null, - 'x-sentry-rate-limits': null, - }, - }), - ); - }); - - it('OK with rate-limit headers', async () => { - await setupTestServer({ - statusCode: SUCCESS, - responseHeaders: { - 'Retry-After': '2700', - 'X-Sentry-Rate-Limits': '60::organization, 2700::organization', - }, - }); - - makeNodeTransport(defaultOptions); - const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; - - const executorResult = registeredRequestExecutor({ - body: serializeEnvelope(EVENT_ENVELOPE), - category: 'error', - }); - - await expect(executorResult).resolves.toEqual( - expect.objectContaining({ - statusCode: SUCCESS, - headers: { - 'retry-after': '2700', - 'x-sentry-rate-limits': '60::organization, 2700::organization', - }, - }), - ); - }); - - it('NOK with rate-limit headers', async () => { - await setupTestServer({ - statusCode: RATE_LIMIT, - responseHeaders: { - 'Retry-After': '2700', - 'X-Sentry-Rate-Limits': '60::organization, 2700::organization', - }, - }); - - makeNodeTransport(defaultOptions); - const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; - - const executorResult = registeredRequestExecutor({ - body: serializeEnvelope(EVENT_ENVELOPE), - category: 'error', - }); - - await expect(executorResult).resolves.toEqual( - expect.objectContaining({ - statusCode: RATE_LIMIT, - headers: { - 'retry-after': '2700', - 'x-sentry-rate-limits': '60::organization, 2700::organization', - }, - }), - ); - }); - }); - - it('should create a noop transport if an invalid url is passed', async () => { - const requestSpy = jest.spyOn(http, 'request'); - const transport = makeNodeTransport({ ...defaultOptions, url: 'foo' }); - await transport.send(EVENT_ENVELOPE); - expect(requestSpy).not.toHaveBeenCalled(); - }); - - it('should warn if an invalid url is passed', async () => { - const transport = makeNodeTransport({ ...defaultOptions, url: 'invalid url' }); - await transport.send(EVENT_ENVELOPE); - expect(consoleWarnSpy).toHaveBeenCalledWith( - '[@sentry/node]: Invalid dsn or tunnel option, will not send any events. The tunnel option must be a full URL when used.', - ); - }); -}); diff --git a/packages/node-experimental/test/transports/https.test.ts b/packages/node-experimental/test/transports/https.test.ts deleted file mode 100644 index 8b0d3312ba54..000000000000 --- a/packages/node-experimental/test/transports/https.test.ts +++ /dev/null @@ -1,387 +0,0 @@ -import * as http from 'http'; -import * as https from 'https'; -import { createTransport } from '@sentry/core'; -import type { EventEnvelope, EventItem } from '@sentry/types'; -import { createEnvelope, serializeEnvelope } from '@sentry/utils'; - -import { makeNodeTransport } from '../../src/transports'; -import type { HTTPModule, HTTPModuleRequestIncomingMessage } from '../../src/transports/http-module'; -import testServerCerts from './test-server-certs'; - -jest.mock('@sentry/core', () => { - const actualCore = jest.requireActual('@sentry/core'); - return { - ...actualCore, - createTransport: jest.fn().mockImplementation(actualCore.createTransport), - }; -}); - -import * as httpProxyAgent from '../../src/proxy'; - -const SUCCESS = 200; -const RATE_LIMIT = 429; -const INVALID = 400; -const FAILED = 500; - -interface TestServerOptions { - statusCode: number; - responseHeaders?: Record; -} - -let testServer: http.Server | undefined; - -function setupTestServer( - options: TestServerOptions, - requestInspector?: (req: http.IncomingMessage, body: string) => void, -) { - testServer = https.createServer(testServerCerts, (req, res) => { - let body = ''; - - req.on('data', data => { - body += data; - }); - - req.on('end', () => { - requestInspector?.(req, body); - }); - - res.writeHead(options.statusCode, options.responseHeaders); - res.end(); - - // also terminate socket because keepalive hangs connection a bit - // eslint-disable-next-line deprecation/deprecation - res.connection?.end(); - }); - - testServer.listen(8100); - - return new Promise(resolve => { - testServer?.on('listening', resolve); - }); -} - -const TEST_SERVER_URL = 'https://localhost:8100'; - -const EVENT_ENVELOPE = createEnvelope({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [ - [{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem, -]); - -const SERIALIZED_EVENT_ENVELOPE = serializeEnvelope(EVENT_ENVELOPE); - -const unsafeHttpsModule: HTTPModule = { - request: jest - .fn() - .mockImplementation((options: https.RequestOptions, callback?: (res: HTTPModuleRequestIncomingMessage) => void) => { - return https.request({ ...options, rejectUnauthorized: false }, callback); - }), -}; - -const defaultOptions = { - httpModule: unsafeHttpsModule, - url: TEST_SERVER_URL, - recordDroppedEvent: () => undefined, // noop -}; - -afterEach(done => { - jest.clearAllMocks(); - - if (testServer && testServer.listening) { - testServer.close(done); - } else { - done(); - } -}); - -describe('makeNewHttpsTransport()', () => { - describe('.send()', () => { - it('should correctly send envelope to server', async () => { - await setupTestServer({ statusCode: SUCCESS }, (req, body) => { - expect(req.method).toBe('POST'); - expect(body).toBe(SERIALIZED_EVENT_ENVELOPE); - }); - - const transport = makeNodeTransport(defaultOptions); - await transport.send(EVENT_ENVELOPE); - }); - - it('should correctly send user-provided headers to server', async () => { - await setupTestServer({ statusCode: SUCCESS }, req => { - expect(req.headers).toEqual( - expect.objectContaining({ - // node http module lower-cases incoming headers - 'x-some-custom-header-1': 'value1', - 'x-some-custom-header-2': 'value2', - }), - ); - }); - - const transport = makeNodeTransport({ - ...defaultOptions, - headers: { - 'X-Some-Custom-Header-1': 'value1', - 'X-Some-Custom-Header-2': 'value2', - }, - }); - - await transport.send(EVENT_ENVELOPE); - }); - - it.each([RATE_LIMIT, INVALID, FAILED])( - 'should resolve on bad server response (status %i)', - async serverStatusCode => { - await setupTestServer({ statusCode: serverStatusCode }); - - const transport = makeNodeTransport(defaultOptions); - expect(() => { - expect(transport.send(EVENT_ENVELOPE)); - }).not.toThrow(); - }, - ); - - it('should resolve when server responds with rate limit header and status code 200', async () => { - await setupTestServer({ - statusCode: SUCCESS, - responseHeaders: { - 'Retry-After': '2700', - 'X-Sentry-Rate-Limits': '60::organization, 2700::organization', - }, - }); - - const transport = makeNodeTransport(defaultOptions); - await expect(transport.send(EVENT_ENVELOPE)).resolves.toEqual({ - statusCode: SUCCESS, - headers: { - 'retry-after': '2700', - 'x-sentry-rate-limits': '60::organization, 2700::organization', - }, - }); - }); - - it('should use `caCerts` option', async () => { - await setupTestServer({ statusCode: SUCCESS }); - - const transport = makeNodeTransport({ - ...defaultOptions, - httpModule: unsafeHttpsModule, - url: TEST_SERVER_URL, - caCerts: 'some cert', - }); - - await transport.send(EVENT_ENVELOPE); - - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(unsafeHttpsModule.request).toHaveBeenCalledWith( - expect.objectContaining({ - ca: 'some cert', - }), - expect.anything(), - ); - }); - }); - - describe('proxy', () => { - const proxyAgentSpy = jest - .spyOn(httpProxyAgent, 'HttpsProxyAgent') - // @ts-expect-error using http agent as https proxy agent - .mockImplementation(() => new http.Agent({ keepAlive: false, maxSockets: 30, timeout: 2000 })); - - it('can be configured through option', () => { - makeNodeTransport({ - ...defaultOptions, - httpModule: unsafeHttpsModule, - url: 'https://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', - proxy: 'https://example.com', - }); - - expect(proxyAgentSpy).toHaveBeenCalledTimes(1); - expect(proxyAgentSpy).toHaveBeenCalledWith('https://example.com'); - }); - - it('can be configured through env variables option (http)', () => { - process.env.http_proxy = 'https://example.com'; - makeNodeTransport({ - ...defaultOptions, - httpModule: unsafeHttpsModule, - url: 'https://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', - }); - - expect(proxyAgentSpy).toHaveBeenCalledTimes(1); - expect(proxyAgentSpy).toHaveBeenCalledWith('https://example.com'); - delete process.env.http_proxy; - }); - - it('can be configured through env variables option (https)', () => { - process.env.https_proxy = 'https://example.com'; - makeNodeTransport({ - ...defaultOptions, - httpModule: unsafeHttpsModule, - url: 'https://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', - }); - - expect(proxyAgentSpy).toHaveBeenCalledTimes(1); - expect(proxyAgentSpy).toHaveBeenCalledWith('https://example.com'); - delete process.env.https_proxy; - }); - - it('client options have priority over env variables', () => { - process.env.https_proxy = 'https://foo.com'; - makeNodeTransport({ - ...defaultOptions, - httpModule: unsafeHttpsModule, - url: 'https://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', - proxy: 'https://bar.com', - }); - - expect(proxyAgentSpy).toHaveBeenCalledTimes(1); - expect(proxyAgentSpy).toHaveBeenCalledWith('https://bar.com'); - delete process.env.https_proxy; - }); - - it('no_proxy allows for skipping specific hosts', () => { - process.env.no_proxy = 'sentry.io'; - makeNodeTransport({ - ...defaultOptions, - httpModule: unsafeHttpsModule, - url: 'https://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', - proxy: 'https://example.com', - }); - - expect(proxyAgentSpy).not.toHaveBeenCalled(); - - delete process.env.no_proxy; - }); - - it('no_proxy works with a port', () => { - process.env.http_proxy = 'https://example.com:8080'; - process.env.no_proxy = 'sentry.io:8989'; - - makeNodeTransport({ - ...defaultOptions, - httpModule: unsafeHttpsModule, - url: 'https://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', - }); - - expect(proxyAgentSpy).not.toHaveBeenCalled(); - - delete process.env.no_proxy; - delete process.env.http_proxy; - }); - - it('no_proxy works with multiple comma-separated hosts', () => { - process.env.http_proxy = 'https://example.com:8080'; - process.env.no_proxy = 'example.com,sentry.io,wat.com:1337'; - - makeNodeTransport({ - ...defaultOptions, - httpModule: unsafeHttpsModule, - url: 'https://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', - }); - - expect(proxyAgentSpy).not.toHaveBeenCalled(); - - delete process.env.no_proxy; - delete process.env.http_proxy; - }); - }); - - it('should register TransportRequestExecutor that returns the correct object from server response (rate limit)', async () => { - await setupTestServer({ - statusCode: RATE_LIMIT, - responseHeaders: {}, - }); - - makeNodeTransport(defaultOptions); - const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; - - const executorResult = registeredRequestExecutor({ - body: serializeEnvelope(EVENT_ENVELOPE), - category: 'error', - }); - - await expect(executorResult).resolves.toEqual( - expect.objectContaining({ - statusCode: RATE_LIMIT, - }), - ); - }); - - it('should register TransportRequestExecutor that returns the correct object from server response (OK)', async () => { - await setupTestServer({ - statusCode: SUCCESS, - }); - - makeNodeTransport(defaultOptions); - const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; - - const executorResult = registeredRequestExecutor({ - body: serializeEnvelope(EVENT_ENVELOPE), - category: 'error', - }); - - await expect(executorResult).resolves.toEqual( - expect.objectContaining({ - statusCode: SUCCESS, - headers: { - 'retry-after': null, - 'x-sentry-rate-limits': null, - }, - }), - ); - }); - - it('should register TransportRequestExecutor that returns the correct object from server response (OK with rate-limit headers)', async () => { - await setupTestServer({ - statusCode: SUCCESS, - responseHeaders: { - 'Retry-After': '2700', - 'X-Sentry-Rate-Limits': '60::organization, 2700::organization', - }, - }); - - makeNodeTransport(defaultOptions); - const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; - - const executorResult = registeredRequestExecutor({ - body: serializeEnvelope(EVENT_ENVELOPE), - category: 'error', - }); - - await expect(executorResult).resolves.toEqual( - expect.objectContaining({ - statusCode: SUCCESS, - headers: { - 'retry-after': '2700', - 'x-sentry-rate-limits': '60::organization, 2700::organization', - }, - }), - ); - }); - - it('should register TransportRequestExecutor that returns the correct object from server response (NOK with rate-limit headers)', async () => { - await setupTestServer({ - statusCode: RATE_LIMIT, - responseHeaders: { - 'Retry-After': '2700', - 'X-Sentry-Rate-Limits': '60::organization, 2700::organization', - }, - }); - - makeNodeTransport(defaultOptions); - const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; - - const executorResult = registeredRequestExecutor({ - body: serializeEnvelope(EVENT_ENVELOPE), - category: 'error', - }); - - await expect(executorResult).resolves.toEqual( - expect.objectContaining({ - statusCode: RATE_LIMIT, - headers: { - 'retry-after': '2700', - 'x-sentry-rate-limits': '60::organization, 2700::organization', - }, - }), - ); - }); -}); diff --git a/packages/node-experimental/test/transports/test-server-certs.ts b/packages/node-experimental/test/transports/test-server-certs.ts deleted file mode 100644 index a5ce436c4234..000000000000 --- a/packages/node-experimental/test/transports/test-server-certs.ts +++ /dev/null @@ -1,48 +0,0 @@ -export default { - key: `-----BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAuMunjXC2tu2d4x8vKuPQbHwPjYG6pVvAUs7wzpDnMEGo3o2A -bZpL7vUAkQWZ86M84rX9b65cVvT35uqM9uxnJKQhSdGARxEcrz9yxjc9RaIO9xM4 -6WdFd6pcVHW9MF6njnc19jyIoSGXRADJjreNZHyMobAHyL2ZbFiptknUWFW3YT4t -q9bQD5yfhZ94fRt1IbdBAn5Bmz6x61BYudWU2KA3G1akPUmzj0OwZwaIrnGbfLUH -M5F50dNUYfCdmxtE8YRBPyWwcg+KOWa/P8C84p1UQ+/0GHNqUTa4wXBgKeUXNjth -AhV/4JgDDdec+/W0Z1UdEqxZvKfAYnjveFpxEwIDAQABAoIBADLsjEPB59gJKxVH -pqvfE7SRi4enVFP1MM6hEGMcM1ls/qg1vkp11q8G/Rz5ui8VsNWY6To5hmDAKQCN -akMxaksCn9nDzeHHqWvxxCMzXcMuoYkc1vYa613KqJ7twzDtJKdx2oD8tXoR06l9 -vg2CL4idefOkmsCK3xioZjxBpC6jF6ybvlY241MGhaAGRHmP6ik1uFJ+6Y8smh6R -AQKO0u0oQPy6bka9F6DTP6BMUeZ+OA/oOrrb5FxTHu8AHcyCSk2wHnCkB9EF/Ou2 -xSWrnu0O0/0Px6OO9oEsNSq2/fKNV9iuEU8LeAoDVm4ysyMrPce2c4ZsB4U244bj -yQpQZ6ECgYEA9KwA7Lmyf+eeZHxEM4MNSqyeXBtSKu4Zyk0RRY1j69ConjHKet3Q -ylVedXQ0/FJAHHKEm4zFGZtnaaxrzCIcQSKJBCoaA+cN44MM3D1nKmHjgPy8R/yE -BNgIVwJB1MmVSGa+NYnQgUomcCIEr/guNMIxV7p2iybqoxaEHKLfGFUCgYEAwVn1 -8LARsZihLUdxxbAc9+v/pBeMTrkTw1eN1ki9VWYoRam2MLozehEzabt677cU4h7+ -bjdKCKo1x2liY9zmbIiVHssv9Jf3E9XhcajsXB42m1+kjUYVPh8o9lDXcatV9EKt -DZK8wfRY9boyDKB2zRyo6bvIEK3qWbas31W3a8cCgYA6w0TFliPkzEAiaiYHKSZ8 -FNFD1dv6K41OJQxM5BRngom81MCImdWXgsFY/DvtjeOP8YEfysNbzxMbMioBsP+Q -NTcrJOFypn+TcNoZ2zV33GLDi++8ak1azHfUTdp5vKB57xMn0J2fL6vjqoftq3GN -gkZPh50I9qPL35CDQCrMsQKBgC6tFfc1uf/Cld5FagzMOCINodguKxvyB/hXUZFS -XAqar8wpbScUPEsSjfPPY50s+GiiDM/0nvW6iWMLaMos0J+Q1VbqvDfy2525O0Ri -ADU4wfv+Oc41BfnKMexMlcYGE6j006v8KX81Cqi/e0ebETLw4UITp/eG1JU1yUPd -AHuPAoGBAL25v4/onoH0FBLdEwb2BAENxc+0g4In1T+83jfHbfD0gOF3XTbgH4FF -MduIG8qBoZC5whiZ3qH7YJK7sydaM1bDwiesqIik+gEUE65T7S2ZF84y5GC5JjTf -z6v6i+DMCIJXDY5/gjzOED6UllV2Jrn2pDoV++zVyR6KAwXpCmK6 ------END RSA PRIVATE KEY-----`, - cert: `-----BEGIN CERTIFICATE----- -MIIDETCCAfkCFCMI53aBdS2kWTrw39Kkv93ErG3iMA0GCSqGSIb3DQEBCwUAMEUx -CzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl -cm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMjIwMzI4MDgzODQwWhcNNDkwODEyMDgz -ODQwWjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UE -CgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOC -AQ8AMIIBCgKCAQEAuMunjXC2tu2d4x8vKuPQbHwPjYG6pVvAUs7wzpDnMEGo3o2A -bZpL7vUAkQWZ86M84rX9b65cVvT35uqM9uxnJKQhSdGARxEcrz9yxjc9RaIO9xM4 -6WdFd6pcVHW9MF6njnc19jyIoSGXRADJjreNZHyMobAHyL2ZbFiptknUWFW3YT4t -q9bQD5yfhZ94fRt1IbdBAn5Bmz6x61BYudWU2KA3G1akPUmzj0OwZwaIrnGbfLUH -M5F50dNUYfCdmxtE8YRBPyWwcg+KOWa/P8C84p1UQ+/0GHNqUTa4wXBgKeUXNjth -AhV/4JgDDdec+/W0Z1UdEqxZvKfAYnjveFpxEwIDAQABMA0GCSqGSIb3DQEBCwUA -A4IBAQBh4BKiByhyvAc5uHj5bkSqspY2xZWW8xiEGaCaQWDMlyjP9mVVWFHfE3XL -lzsJdZVnHDZUliuA5L+qTEpLJ5GmgDWqnKp3HdhtkL16mPbPyJLPY0X+m7wvoZRt -RwLfFCx1E13m0ktYWWgmSCnBl+rI7pyagDhZ2feyxsMrecCazyG/llFBuyWSOnIi -OHxjdHV7be5c8uOOp1iNB9j++LW1pRVrSCWOKRLcsUBal73FW+UvhM5+1If/F9pF -GNQrMhVRA8aHD0JAu3tpjYRKRuOpAbbqtiAUSbDPsJBQy/K9no2K83G7+AV+aGai -HXfQqFFJS6xGKU79azH51wLVEGXq ------END CERTIFICATE-----`, -}; diff --git a/packages/node-experimental/tsconfig.json b/packages/node-experimental/tsconfig.json deleted file mode 100644 index 8f38d240197e..000000000000 --- a/packages/node-experimental/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.json", - - "include": ["src/**/*"], - - "compilerOptions": { - "lib": ["es2018"], - "module": "Node16" - } -} diff --git a/packages/node/LICENSE b/packages/node/LICENSE index 535ef0561e1b..d11896ba1181 100644 --- a/packages/node/LICENSE +++ b/packages/node/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019 Sentry (https://sentry.io) and individual contributors. All rights reserved. +Copyright (c) 2023 Sentry (https://sentry.io) and individual contributors. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the diff --git a/packages/node/README.md b/packages/node/README.md index af6819c9a4ce..20fe8cc175c3 100644 --- a/packages/node/README.md +++ b/packages/node/README.md @@ -4,36 +4,28 @@

    -# Legacy Sentry SDK for NodeJS +# Official Sentry SDK for Node -[![npm version](https://img.shields.io/npm/v/@sentry/node-experimental.svg)](https://www.npmjs.com/package/@sentry/node-experimental) -[![npm dm](https://img.shields.io/npm/dm/@sentry/node-experimental.svg)](https://www.npmjs.com/package/@sentry/node-experimental) -[![npm dt](https://img.shields.io/npm/dt/@sentry/node-experimental.svg)](https://www.npmjs.com/package/@sentry/node-experimental) +[![npm version](https://img.shields.io/npm/v/@sentry/node.svg)](https://www.npmjs.com/package/@sentry/node) +[![npm dm](https://img.shields.io/npm/dm/@sentry/node.svg)](https://www.npmjs.com/package/@sentry/node) +[![npm dt](https://img.shields.io/npm/dt/@sentry/node.svg)](https://www.npmjs.com/package/@sentry/node) -## Links - -- [Official SDK Docs](https://docs.sentry.io/quickstart/) -- [TypeDoc](http://getsentry.github.io/sentry-javascript/) +## Installation -## Status +```bash +npm install @sentry/node -Since v8, this is the _legacy_ SDK, and it will most likely be completely removed before v8 is fully stable. It only -exists so that Meta-SDKs like `@sentry/nextjs` or `@sentry/sveltekit` can be migrated to the new `@sentry/node` -step-by-step. - -You should instead use [@sentry/node](./../node-experimental/). +# Or yarn +yarn add @sentry/node +``` ## Usage -To use this SDK, call `init(options)` as early as possible in the main entry module. This will initialize the SDK and -hook into the environment. Note that you can turn off almost all side effects using the respective options. Minimum -supported Node version is Node 14. - -```javascript -// CJS syntax -const Sentry = require('@sentry/node-experimental'); -// ESM syntax -import * as Sentry from '@sentry/node-experimental'; +```js +// CJS Syntax +const Sentry = require('@sentry/node'); +// ESM Syntax +import * as Sentry from '@sentry/node'; Sentry.init({ dsn: '__DSN__', @@ -41,28 +33,27 @@ Sentry.init({ }); ``` -To set context information or send manual events, use the exported functions of `@sentry/node-experimental`. Note that -these functions will not perform any action before you have called `init()`: +Note that it is necessary to initialize Sentry **before you import any package that may be instrumented by us**. -```javascript -// Set user information, as well as tags and further extras -Sentry.setExtra('battery', 0.7); -Sentry.setTag('user_mode', 'admin'); -Sentry.setUser({ id: '4711' }); +[More information on how to set up Sentry for Node in v8.](https://github.com/getsentry/sentry-javascript/blob/develop/docs/v8-node.md) -// Add a breadcrumb for future events -Sentry.addBreadcrumb({ - message: 'My Breadcrumb', - // ... -}); +### ESM Support -// Capture exceptions, messages or manual events -Sentry.captureMessage('Hello, world!'); -Sentry.captureException(new Error('Good bye')); -Sentry.captureEvent({ - message: 'Manual', - stacktrace: [ - // ... - ], -}); +Due to the way OpenTelemetry handles instrumentation, this only works out of the box for CommonJS (`require`) +applications. + +There is experimental support for running OpenTelemetry with ESM (`"type": "module"`): + +```bash +node --experimental-loader=@opentelemetry/instrumentation/hook.mjs ./app.js ``` + +You'll need to install `@opentelemetry/instrumentation` in your app to ensure this works. + +See +[OpenTelemetry Instrumentation Docs](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation#instrumentation-for-es-modules-in-nodejs-experimental) +for details on this - but note that this is a) experimental, and b) does not work with all integrations. + +## Links + +- [Official SDK Docs](https://docs.sentry.io/quickstart/) diff --git a/packages/node/package.json b/packages/node/package.json index f71fdbbdc24d..61c6f7d932d4 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -1,7 +1,7 @@ { - "name": "@sentry/node-experimental", + "name": "@sentry/node", "version": "8.0.0-alpha.7", - "description": "The old version of Sentry SDK for Node.js, without OpenTelemetry support.", + "description": "Sentry Node SDK using OpenTelemetry for performance instrumentation", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/node", "author": "Sentry", @@ -13,7 +13,8 @@ "cjs", "esm", "types", - "types-ts3.8" + "types-ts3.8", + "register.mjs" ], "main": "build/cjs/index.js", "module": "build/esm/index.js", @@ -29,6 +30,16 @@ "types": "./build/types/index.d.ts", "default": "./build/cjs/index.js" } + }, + "./register": { + "import": { + "default": "./build/register.mjs" + } + }, + "./hook": { + "import": { + "default": "./build/hook.mjs" + } } }, "typesVersions": { @@ -42,22 +53,39 @@ "access": "public" }, "dependencies": { - "@sentry-internal/tracing": "8.0.0-alpha.7", + "@opentelemetry/api": "1.7.0", + "@opentelemetry/context-async-hooks": "1.21.0", + "@opentelemetry/core": "1.21.0", + "@opentelemetry/instrumentation": "0.48.0", + "@opentelemetry/instrumentation-express": "0.35.0", + "@opentelemetry/instrumentation-fastify": "0.33.0", + "@opentelemetry/instrumentation-graphql": "0.37.0", + "@opentelemetry/instrumentation-hapi": "0.34.0", + "@opentelemetry/instrumentation-http": "0.48.0", + "@opentelemetry/instrumentation-koa": "0.37.0", + "@opentelemetry/instrumentation-mongodb": "0.39.0", + "@opentelemetry/instrumentation-mongoose": "0.35.0", + "@opentelemetry/instrumentation-mysql": "0.35.0", + "@opentelemetry/instrumentation-mysql2": "0.35.0", + "@opentelemetry/instrumentation-nestjs-core": "0.34.0", + "@opentelemetry/instrumentation-pg": "0.38.0", + "@opentelemetry/resources": "1.21.0", + "@opentelemetry/sdk-trace-base": "1.21.0", + "@opentelemetry/semantic-conventions": "1.21.0", + "@prisma/instrumentation": "5.9.0", "@sentry/core": "8.0.0-alpha.7", + "@sentry/opentelemetry": "8.0.0-alpha.7", "@sentry/types": "8.0.0-alpha.7", "@sentry/utils": "8.0.0-alpha.7" }, "devDependencies": { - "@types/cookie": "0.5.2", - "@types/express": "^4.17.14", - "@types/lru-cache": "^5.1.0", - "@types/node": "14.18.63", - "express": "^4.17.1", - "nock": "^13.0.5", - "undici": "^5.21.0" + "@types/node": "^14.18.0" + }, + "optionalDependencies": { + "opentelemetry-instrumentation-fetch-node": "1.1.2" }, "scripts": { - "build": "run-s build:transpile build:types", + "build": "run-p build:transpile build:types", "build:dev": "yarn build", "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "run-s build:types:core build:types:downlevel", @@ -72,23 +100,13 @@ "clean": "rimraf build coverage sentry-node-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", - "test": "run-s test:jest test:express test:webpack test:release-health", - "test:express": "node test/manual/express-scope-separation/start.js", + "test": "yarn test:jest", "test:jest": "jest", - "test:release-health": "node test/manual/release-health/runner.js", - "test:webpack": "cd test/manual/webpack-async-context/ && yarn --silent --ignore-engines && node npm-build.js", "test:watch": "jest --watch", "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push --sig" }, "volta": { "extends": "../../package.json" }, - "madge": { - "detectiveOptions": { - "ts": { - "skipTypeImports": true - } - } - }, "sideEffects": false } diff --git a/packages/node/rollup.anr-worker.config.mjs b/packages/node/rollup.anr-worker.config.mjs index 48463d5763ee..bd3c1d4b825c 100644 --- a/packages/node/rollup.anr-worker.config.mjs +++ b/packages/node/rollup.anr-worker.config.mjs @@ -1,34 +1,31 @@ import { makeBaseBundleConfig } from '@sentry-internal/rollup-utils'; -function createAnrWorkerConfig(destDir, esm) { - return makeBaseBundleConfig({ - bundleType: 'node-worker', - entrypoints: ['src/integrations/anr/worker.ts'], - licenseTitle: '@sentry/node', - outputFileBase: () => 'worker-script.js', - packageSpecificConfig: { - output: { - dir: destDir, - sourcemap: false, - }, - plugins: [ - { - name: 'output-base64-worker-script', - renderChunk(code) { - const base64Code = Buffer.from(code).toString('base64'); - if (esm) { - return `export const base64WorkerScript = '${base64Code}';`; - } else { - return `exports.base64WorkerScript = '${base64Code}';`; - } - }, +export function createAnrWorkerCode() { + let base64Code; + + return { + workerRollupConfig: makeBaseBundleConfig({ + bundleType: 'node-worker', + entrypoints: ['src/integrations/anr/worker.ts'], + licenseTitle: '@sentry/node', + outputFileBase: () => 'worker-script.js', + packageSpecificConfig: { + output: { + dir: 'build/esm/integrations/anr', + sourcemap: false, }, - ], + plugins: [ + { + name: 'output-base64-worker-script', + renderChunk(code) { + base64Code = Buffer.from(code).toString('base64'); + }, + }, + ], + }, + }), + getBase64Code() { + return base64Code; }, - }); + }; } - -export const anrWorkerConfigs = [ - createAnrWorkerConfig('build/esm/integrations/anr', true), - createAnrWorkerConfig('build/cjs/integrations/anr', false), -]; diff --git a/packages/node/rollup.npm.config.mjs b/packages/node/rollup.npm.config.mjs index 88c90de4825f..9622acb20112 100644 --- a/packages/node/rollup.npm.config.mjs +++ b/packages/node/rollup.npm.config.mjs @@ -1,8 +1,36 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; -import { anrWorkerConfigs } from './rollup.anr-worker.config.mjs'; +import replace from '@rollup/plugin-replace'; +import { makeBaseNPMConfig, makeNPMConfigVariants, makeOtelLoaders } from '@sentry-internal/rollup-utils'; +import { createAnrWorkerCode } from './rollup.anr-worker.config.mjs'; + +const { workerRollupConfig, getBase64Code } = createAnrWorkerCode(); export default [ - ...makeNPMConfigVariants(makeBaseNPMConfig()), - // The ANR worker builds must come after the main build because they overwrite the worker-script.js file - ...anrWorkerConfigs, + ...makeOtelLoaders('./build', 'otel'), + // The worker needs to be built first since it's output is used in the main bundle. + workerRollupConfig, + ...makeNPMConfigVariants( + makeBaseNPMConfig({ + packageSpecificConfig: { + output: { + // set exports to 'named' or 'auto' so that rollup doesn't warn + exports: 'named', + // set preserveModules to false because we want to bundle everything into one file. + preserveModules: + process.env.SENTRY_BUILD_PRESERVE_MODULES === undefined + ? false + : Boolean(process.env.SENTRY_BUILD_PRESERVE_MODULES), + }, + plugins: [ + replace({ + delimiters: ['###', '###'], + // removes some webpack warnings + preventAssignment: true, + values: { + base64WorkerScript: () => getBase64Code(), + }, + }), + ], + }, + }), + ), ]; diff --git a/packages/node/src/_setSpanForScope.ts b/packages/node/src/_setSpanForScope.ts deleted file mode 100644 index e0abd9691b09..000000000000 --- a/packages/node/src/_setSpanForScope.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { Scope, Span } from '@sentry/types'; -import { addNonEnumerableProperty } from '@sentry/utils'; - -// This is inlined here from packages/core/src/utils/spanUtils.ts to avoid exporting this from there -// ------------------------ - -const SCOPE_SPAN_FIELD = '_sentrySpan'; - -type ScopeWithMaybeSpan = Scope & { - [SCOPE_SPAN_FIELD]?: Span; -}; - -/** - * Set the active span for a given scope. - * NOTE: This should NOT be used directly, but is only used internally by the trace methods. - */ -export function _setSpanForScope(scope: Scope, span: Span | undefined): void { - if (span) { - addNonEnumerableProperty(scope as ScopeWithMaybeSpan, SCOPE_SPAN_FIELD, span); - } else { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete (scope as ScopeWithMaybeSpan)[SCOPE_SPAN_FIELD]; - } -} diff --git a/packages/node/src/async/domain.ts b/packages/node/src/async/domain.ts deleted file mode 100644 index 904f949441de..000000000000 --- a/packages/node/src/async/domain.ts +++ /dev/null @@ -1,122 +0,0 @@ -import * as domain from 'domain'; -import { getGlobalHub } from '@sentry/core'; -import { Hub as HubClass } from '@sentry/core'; -import { setAsyncContextStrategy } from '@sentry/core'; -import type { Client, Hub, Scope } from '@sentry/types'; - -type DomainWithHub = domain.Domain & { - hub?: Hub; -}; - -function getActiveDomain(): T | undefined { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any - return (domain as any).active as T | undefined; -} - -function getCurrentDomainHub(): Hub | undefined { - const activeDomain = getActiveDomain(); - - // If there's no active domain, just return undefined and the global hub will be used - if (!activeDomain) { - return undefined; - } - - if (activeDomain.hub) { - return activeDomain.hub; - } - - activeDomain.hub = getCurrentHub(); - return activeDomain.hub; -} - -function getCurrentHub(): Hub { - return getCurrentDomainHub() || getGlobalHub(); -} - -function withExecutionContext( - client: Client | undefined, - scope: Scope, - isolationScope: Scope, - callback: () => T, -): T { - const local = domain.create() as DomainWithHub; - - // eslint-disable-next-line deprecation/deprecation - const newHub = new HubClass(client, scope, isolationScope); - local.hub = newHub; - - return local.bind(() => { - return callback(); - })(); -} - -function withScope(callback: (scope: Scope) => T): T { - const parentHub = getCurrentHub(); - - /* eslint-disable deprecation/deprecation */ - const client = parentHub.getClient(); - const scope = parentHub.getScope().clone(); - const isolationScope = parentHub.getIsolationScope(); - /* eslint-enable deprecation/deprecation */ - - return withExecutionContext(client, scope, isolationScope, () => { - return callback(scope); - }); -} - -function withSetScope(scope: Scope, callback: (scope: Scope) => T): T { - const parentHub = getCurrentHub(); - - /* eslint-disable deprecation/deprecation */ - const client = parentHub.getClient(); - const isolationScope = parentHub.getIsolationScope(); - /* eslint-enable deprecation/deprecation */ - - return withExecutionContext(client, scope, isolationScope, () => { - return callback(scope); - }); -} - -function withIsolationScope(callback: (isolationScope: Scope) => T): T { - const parentHub = getCurrentHub(); - - /* eslint-disable deprecation/deprecation */ - const client = parentHub.getClient(); - const scope = parentHub.getScope().clone(); - const isolationScope = parentHub.getIsolationScope().clone(); - /* eslint-enable deprecation/deprecation */ - - return withExecutionContext(client, scope, isolationScope, () => { - return callback(isolationScope); - }); -} - -function withSetIsolationScope(isolationScope: Scope, callback: (isolationScope: Scope) => T): T { - const parentHub = getCurrentHub(); - - /* eslint-disable deprecation/deprecation */ - const client = parentHub.getClient(); - const scope = parentHub.getScope().clone(); - /* eslint-enable deprecation/deprecation */ - - return withExecutionContext(client, scope, isolationScope, () => { - return callback(scope); - }); -} - -/** - * Sets the async context strategy to use Node.js domains. - */ -export function setDomainAsyncContextStrategy(): void { - setAsyncContextStrategy({ - getCurrentHub, - withScope, - withSetScope, - withIsolationScope, - withSetIsolationScope, - // eslint-disable-next-line deprecation/deprecation - getCurrentScope: () => getCurrentHub().getScope(), - // eslint-disable-next-line deprecation/deprecation - getIsolationScope: () => getCurrentHub().getIsolationScope(), - }); -} diff --git a/packages/node/src/async/hooks.ts b/packages/node/src/async/hooks.ts deleted file mode 100644 index 16a5c8ee4364..000000000000 --- a/packages/node/src/async/hooks.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { Hub as HubClass, getGlobalHub } from '@sentry/core'; -import { setAsyncContextStrategy } from '@sentry/core'; -import type { Hub, Scope } from '@sentry/types'; -import * as async_hooks from 'async_hooks'; - -interface AsyncLocalStorage { - getStore(): T | undefined; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - run(store: T, callback: (...args: TArgs) => R, ...args: TArgs): R; -} - -type AsyncLocalStorageConstructor = { new (): AsyncLocalStorage }; -// AsyncLocalStorage only exists in async_hook after Node v12.17.0 or v13.10.0 -type NewerAsyncHooks = typeof async_hooks & { AsyncLocalStorage: AsyncLocalStorageConstructor }; - -let asyncStorage: AsyncLocalStorage; - -/** - * Sets the async context strategy to use AsyncLocalStorage which requires Node v12.17.0 or v13.10.0. - */ -export function setHooksAsyncContextStrategy(): void { - if (!asyncStorage) { - asyncStorage = new (async_hooks as NewerAsyncHooks).AsyncLocalStorage(); - } - - function getCurrentHooksHub(): Hub | undefined { - return asyncStorage.getStore(); - } - - function getCurrentHub(): Hub { - return getCurrentHooksHub() || getGlobalHub(); - } - - function withScope(callback: (scope: Scope) => T): T { - const parentHub = getCurrentHub(); - - /* eslint-disable deprecation/deprecation */ - const client = parentHub.getClient(); - const scope = parentHub.getScope().clone(); - const isolationScope = parentHub.getIsolationScope(); - const newHub = new HubClass(client, scope, isolationScope); - /* eslint-enable deprecation/deprecation */ - - return asyncStorage.run(newHub, () => { - return callback(scope); - }); - } - - function withSetScope(scope: Scope, callback: (scope: Scope) => T): T { - const parentHub = getCurrentHub(); - - /* eslint-disable deprecation/deprecation */ - const client = parentHub.getClient(); - const isolationScope = parentHub.getIsolationScope(); - const newHub = new HubClass(client, scope, isolationScope); - /* eslint-enable deprecation/deprecation */ - - return asyncStorage.run(newHub, () => { - return callback(scope); - }); - } - - function withIsolationScope(callback: (isolationScope: Scope) => T): T { - const parentHub = getCurrentHub(); - - /* eslint-disable deprecation/deprecation */ - const client = parentHub.getClient(); - const scope = parentHub.getScope().clone(); - const isolationScope = parentHub.getIsolationScope().clone(); - const newHub = new HubClass(client, scope, isolationScope); - /* eslint-enable deprecation/deprecation */ - - return asyncStorage.run(newHub, () => { - return callback(isolationScope); - }); - } - - function withSetIsolationScope(isolationScope: Scope, callback: (isolationScope: Scope) => T): T { - const parentHub = getCurrentHub(); - - /* eslint-disable deprecation/deprecation */ - const client = parentHub.getClient(); - const scope = parentHub.getScope().clone(); - const newHub = new HubClass(client, scope, isolationScope); - /* eslint-enable deprecation/deprecation */ - - return asyncStorage.run(newHub, () => { - return callback(isolationScope); - }); - } - - setAsyncContextStrategy({ - getCurrentHub, - withScope, - withSetScope, - withIsolationScope, - withSetIsolationScope, - // eslint-disable-next-line deprecation/deprecation - getCurrentScope: () => getCurrentHub().getScope(), - // eslint-disable-next-line deprecation/deprecation - getIsolationScope: () => getCurrentHub().getIsolationScope(), - }); -} diff --git a/packages/node/src/async/index.ts b/packages/node/src/async/index.ts deleted file mode 100644 index a9563e4f0ce6..000000000000 --- a/packages/node/src/async/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { NODE_VERSION } from '../nodeVersion'; -import { setDomainAsyncContextStrategy } from './domain'; -import { setHooksAsyncContextStrategy } from './hooks'; - -/** - * Sets the correct async context strategy for Node.js - * - * Node.js >= 14 uses AsyncLocalStorage - * Node.js < 14 uses domains - */ -export function setNodeAsyncContextStrategy(): void { - if (NODE_VERSION.major >= 14) { - setHooksAsyncContextStrategy(); - } else { - setDomainAsyncContextStrategy(); - } -} diff --git a/packages/node/src/client.ts b/packages/node/src/client.ts deleted file mode 100644 index 5fbb7b208724..000000000000 --- a/packages/node/src/client.ts +++ /dev/null @@ -1,30 +0,0 @@ -import * as os from 'os'; -import type { ServerRuntimeClientOptions } from '@sentry/core'; -import { ServerRuntimeClient, applySdkMetadata } from '@sentry/core'; - -import type { NodeClientOptions } from './types'; - -/** - * The Sentry Node SDK Client. - * - * @see NodeClientOptions for documentation on configuration options. - * @see SentryClient for usage documentation. - */ -export class NodeClient extends ServerRuntimeClient { - /** - * Creates a new Node SDK instance. - * @param options Configuration options for this SDK. - */ - public constructor(options: NodeClientOptions) { - applySdkMetadata(options, 'node'); - - const clientOptions: ServerRuntimeClientOptions = { - ...options, - platform: 'node', - runtime: { name: 'node', version: global.process.version }, - serverName: options.serverName || global.process.env.SENTRY_NAME || os.hostname(), - }; - - super(clientOptions); - } -} diff --git a/packages/node-experimental/src/cron/index.ts b/packages/node/src/cron/index.ts similarity index 100% rename from packages/node-experimental/src/cron/index.ts rename to packages/node/src/cron/index.ts diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts deleted file mode 100644 index b501a79a9231..000000000000 --- a/packages/node/src/handlers.ts +++ /dev/null @@ -1,342 +0,0 @@ -import type * as http from 'http'; -/* eslint-disable @typescript-eslint/no-explicit-any */ -import type { Transaction } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, - captureException, - continueTrace, - flush, - getActiveSpan, - getClient, - getCurrentScope, - getIsolationScope, - hasTracingEnabled, - setHttpStatus, - startInactiveSpan, - withIsolationScope, - withScope, -} from '@sentry/core'; -import type { Span } from '@sentry/types'; -import type { AddRequestDataToEventOptions } from '@sentry/utils'; -import { - addRequestDataToTransaction, - extractPathForTransaction, - isString, - isThenable, - logger, - normalize, -} from '@sentry/utils'; - -import { _setSpanForScope } from './_setSpanForScope'; -import type { NodeClient } from './client'; -import { DEBUG_BUILD } from './debug-build'; -import { isAutoSessionTrackingEnabled } from './sdk'; - -/** - * Express-compatible tracing handler. - * @see Exposed as `Handlers.tracingHandler` - */ -export function tracingHandler(): ( - req: http.IncomingMessage, - res: http.ServerResponse, - next: (error?: any) => void, -) => void { - return function sentryTracingMiddleware( - req: http.IncomingMessage, - res: http.ServerResponse, - next: (error?: any) => void, - ): void { - const options = getClient()?.getOptions(); - - if (req.method?.toUpperCase() === 'OPTIONS' || req.method?.toUpperCase() === 'HEAD') { - return next(); - } - - const sentryTrace = req.headers && isString(req.headers['sentry-trace']) ? req.headers['sentry-trace'] : undefined; - const baggage = req.headers?.baggage; - if (!hasTracingEnabled(options)) { - return next(); - } - - // We depend here on the fact that we update the current scope... - // so we keep this legacy behavior here for now - const scope = getCurrentScope(); - - const [name, source] = extractPathForTransaction(req, { path: true, method: true }); - const transaction = continueTrace({ sentryTrace, baggage }, () => { - scope.setPropagationContext(getCurrentScope().getPropagationContext()); - return startInactiveSpan({ - name, - op: 'http.server', - forceTransaction: true, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.node.tracingHandler', - }, - }) as Transaction; - }); - - // We put the transaction on the scope so users can attach children to it - _setSpanForScope(getCurrentScope(), transaction); - - // We also set __sentry_transaction on the response so people can grab the transaction there to add - // spans to it later. - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - (res as any).__sentry_transaction = transaction; - - res.once('finish', () => { - // Push `transaction.finish` to the next event loop so open spans have a chance to finish before the transaction - // closes - setImmediate(() => { - addRequestDataToTransaction(transaction, req); - setHttpStatus(transaction, res.statusCode); - transaction.end(); - }); - }); - - next(); - }; -} - -export type RequestHandlerOptions = AddRequestDataToEventOptions & { - flushTimeout?: number; -}; - -/** - * Express compatible request handler. - * @see Exposed as `Handlers.requestHandler` - */ -export function requestHandler( - options?: RequestHandlerOptions, -): (req: http.IncomingMessage, res: http.ServerResponse, next: (error?: any) => void) => void { - const client = getClient(); - // Initialise an instance of SessionFlusher on the client when `autoSessionTracking` is enabled and the - // `requestHandler` middleware is used indicating that we are running in SessionAggregates mode - if (client && isAutoSessionTrackingEnabled(client)) { - client.initSessionFlusher(); - - // If Scope contains a Single mode Session, it is removed in favor of using Session Aggregates mode - const isolationScope = getIsolationScope(); - if (isolationScope.getSession()) { - isolationScope.setSession(); - } - } - - return function sentryRequestMiddleware( - req: http.IncomingMessage, - res: http.ServerResponse, - next: (error?: any) => void, - ): void { - if (options && options.flushTimeout && options.flushTimeout > 0) { - // eslint-disable-next-line @typescript-eslint/unbound-method - const _end = res.end; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore I've only updated the node types and this package will soon be removed - res.end = function (chunk?: any | (() => void), encoding?: string | (() => void), cb?: () => void): void { - void flush(options.flushTimeout) - .then(() => { - _end.call(this, chunk, encoding, cb); - }) - .then(null, e => { - DEBUG_BUILD && logger.error(e); - _end.call(this, chunk, encoding, cb); - }); - }; - } - return withIsolationScope(isolationScope => { - isolationScope.setSDKProcessingMetadata({ - request: req, - }); - - const client = getClient(); - if (isAutoSessionTrackingEnabled(client)) { - // Set `status` of `RequestSession` to Ok, at the beginning of the request - isolationScope.setRequestSession({ status: 'ok' }); - } - - res.once('finish', () => { - const client = getClient(); - if (isAutoSessionTrackingEnabled(client)) { - setImmediate(() => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (client && (client as any)._captureRequestSession) { - // Calling _captureRequestSession to capture request session at the end of the request by incrementing - // the correct SessionAggregates bucket i.e. crashed, errored or exited - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - (client as any)._captureRequestSession(); - } - }); - } - }); - next(); - }); - }; -} - -/** JSDoc */ -interface MiddlewareError extends Error { - status?: number | string; - statusCode?: number | string; - status_code?: number | string; - output?: { - statusCode?: number | string; - }; -} - -/** JSDoc */ -function getStatusCodeFromResponse(error: MiddlewareError): number { - const statusCode = error.status || error.statusCode || error.status_code || (error.output && error.output.statusCode); - return statusCode ? parseInt(statusCode as string, 10) : 500; -} - -/** Returns true if response code is internal server error */ -function defaultShouldHandleError(error: MiddlewareError): boolean { - const status = getStatusCodeFromResponse(error); - return status >= 500; -} - -/** - * Express compatible error handler. - * @see Exposed as `Handlers.errorHandler` - */ -export function errorHandler(options?: { - /** - * Callback method deciding whether error should be captured and sent to Sentry - * @param error Captured middleware error - */ - shouldHandleError?(this: void, error: MiddlewareError): boolean; -}): ( - error: MiddlewareError, - req: http.IncomingMessage, - res: http.ServerResponse, - next: (error: MiddlewareError) => void, -) => void { - return function sentryErrorMiddleware( - error: MiddlewareError, - _req: http.IncomingMessage, - res: http.ServerResponse, - next: (error: MiddlewareError) => void, - ): void { - const shouldHandleError = (options && options.shouldHandleError) || defaultShouldHandleError; - - if (shouldHandleError(error)) { - withScope(_scope => { - // The request should already have been stored in `scope.sdkProcessingMetadata` by `sentryRequestMiddleware`, - // but on the off chance someone is using `sentryErrorMiddleware` without `sentryRequestMiddleware`, it doesn't - // hurt to be sure - getIsolationScope().setSDKProcessingMetadata({ request: _req }); - - // For some reason we need to set the transaction on the scope again - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const transaction = (res as any).__sentry_transaction as Span; - if (transaction && !getActiveSpan()) { - _setSpanForScope(_scope, transaction); - } - - const client = getClient(); - if (client && isAutoSessionTrackingEnabled(client)) { - // Check if the `SessionFlusher` is instantiated on the client to go into this branch that marks the - // `requestSession.status` as `Crashed`, and this check is necessary because the `SessionFlusher` is only - // instantiated when the the`requestHandler` middleware is initialised, which indicates that we should be - // running in SessionAggregates mode - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const isSessionAggregatesMode = (client as any)._sessionFlusher !== undefined; - if (isSessionAggregatesMode) { - const requestSession = getIsolationScope().getRequestSession(); - // If an error bubbles to the `errorHandler`, then this is an unhandled error, and should be reported as a - // Crashed session. The `_requestSession.status` is checked to ensure that this error is happening within - // the bounds of a request, and if so the status is updated - if (requestSession && requestSession.status !== undefined) { - requestSession.status = 'crashed'; - } - } - } - - const eventId = captureException(error, { mechanism: { type: 'middleware', handled: false } }); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - (res as any).sentry = eventId; - next(error); - }); - - return; - } - - next(error); - }; -} - -interface SentryTrpcMiddlewareOptions { - /** Whether to include procedure inputs in reported events. Defaults to `false`. */ - attachRpcInput?: boolean; -} - -interface TrpcMiddlewareArguments { - path: string; - type: string; - next: () => T; - rawInput: unknown; -} - -/** - * Sentry tRPC middleware that names the handling transaction after the called procedure. - * - * Use the Sentry tRPC middleware in combination with the Sentry server integration, - * e.g. Express Request Handlers or Next.js SDK. - */ -export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) { - return function ({ path, type, next, rawInput }: TrpcMiddlewareArguments): T { - const clientOptions = getClient()?.getOptions(); - // eslint-disable-next-line deprecation/deprecation - const sentryTransaction = getCurrentScope().getTransaction(); - - if (sentryTransaction) { - sentryTransaction.updateName(`trpc/${path}`); - sentryTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); - sentryTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'rpc.server'); - - const trpcContext: Record = { - procedure_type: type, - }; - - if (options.attachRpcInput !== undefined ? options.attachRpcInput : clientOptions?.sendDefaultPii) { - trpcContext.input = normalize(rawInput); - } - - // TODO: Can we rewrite this to an attribute? Or set this on the scope? - // eslint-disable-next-line deprecation/deprecation - sentryTransaction.setContext('trpc', trpcContext); - } - - function captureIfError(nextResult: { ok: false; error?: Error } | { ok: true }): void { - if (!nextResult.ok) { - captureException(nextResult.error, { mechanism: { handled: false, data: { function: 'trpcMiddleware' } } }); - } - } - - let maybePromiseResult; - try { - maybePromiseResult = next(); - } catch (e) { - captureException(e, { mechanism: { handled: false, data: { function: 'trpcMiddleware' } } }); - throw e; - } - - if (isThenable(maybePromiseResult)) { - Promise.resolve(maybePromiseResult).then( - nextResult => { - captureIfError(nextResult as any); - }, - e => { - captureException(e, { mechanism: { handled: false, data: { function: 'trpcMiddleware' } } }); - }, - ); - } else { - captureIfError(maybePromiseResult as any); - } - - // We return the original promise just to be safe. - return maybePromiseResult; - }; -} diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 36b27086ef08..0960d1a12762 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -1,150 +1,124 @@ -export type { - Breadcrumb, - BreadcrumbHint, - PolymorphicRequest, - Request, - SdkInfo, - Event, - EventHint, - Exception, - Session, - SeverityLevel, - Span, - StackFrame, - Stacktrace, - Thread, - Transaction, - User, -} from '@sentry/types'; -export type { AddRequestDataToEventOptions, TransactionNamingScheme } from '@sentry/utils'; +export { httpIntegration } from './integrations/http'; +export { nativeNodeFetchIntegration } from './integrations/node-fetch'; + +export { consoleIntegration } from './integrations/console'; +export { nodeContextIntegration } from './integrations/context'; +export { contextLinesIntegration } from './integrations/contextlines'; +export { localVariablesIntegration } from './integrations/local-variables'; +export { modulesIntegration } from './integrations/modules'; +export { onUncaughtExceptionIntegration } from './integrations/onuncaughtexception'; +export { onUnhandledRejectionIntegration } from './integrations/onunhandledrejection'; +export { anrIntegration } from './integrations/anr'; + +export { expressIntegration, expressErrorHandler, setupExpressErrorHandler } from './integrations/tracing/express'; +export { fastifyIntegration, setupFastifyErrorHandler } from './integrations/tracing/fastify'; +export { graphqlIntegration } from './integrations/tracing/graphql'; +export { mongoIntegration } from './integrations/tracing/mongo'; +export { mongooseIntegration } from './integrations/tracing/mongoose'; +export { mysqlIntegration } from './integrations/tracing/mysql'; +export { mysql2Integration } from './integrations/tracing/mysql2'; +export { nestIntegration, setupNestErrorHandler } from './integrations/tracing/nest'; +export { postgresIntegration } from './integrations/tracing/postgres'; +export { prismaIntegration } from './integrations/tracing/prisma'; +export { hapiIntegration, setupHapiErrorHandler } from './integrations/tracing/hapi'; +export { koaIntegration, setupKoaErrorHandler } from './integrations/tracing/koa'; +export { spotlightIntegration } from './integrations/spotlight'; + +export { init, getDefaultIntegrations } from './sdk/init'; +export { initOpenTelemetry } from './sdk/initOtel'; +export { getAutoPerformanceIntegrations } from './integrations/tracing'; +export { getSentryRelease, defaultStackParser } from './sdk/api'; +export { createGetModuleFromFilename } from './utils/module'; +export { makeNodeTransport } from './transports'; +export { NodeClient } from './sdk/client'; +export { cron } from './cron'; export type { NodeOptions } from './types'; +export { addRequestDataToEvent, DEFAULT_USER_INCLUDES, extractRequestData } from '@sentry/utils'; + +// These are custom variants that need to be used instead of the core one +// As they have slightly different implementations +export { continueTrace } from '@sentry/opentelemetry'; + export { - addEventProcessor, addBreadcrumb, - addIntegration, - captureException, - captureEvent, - captureMessage, + isInitialized, + getGlobalScope, close, createTransport, flush, - // eslint-disable-next-line deprecation/deprecation - getCurrentHub, - getClient, - isInitialized, - getCurrentScope, - getGlobalScope, - getIsolationScope, Hub, - setCurrentClient, - Scope, SDK_VERSION, - setContext, - setExtra, - setExtras, - setTag, - setTags, - setUser, getSpanStatusFromHttpCode, setHttpStatus, - withScope, - withIsolationScope, captureCheckIn, withMonitor, - setMeasurement, - getActiveSpan, - getRootSpan, - startSpan, - startInactiveSpan, - startSpanManual, - withActiveSpan, - getSpanDescendants, - continueTrace, - parameterize, + requestDataIntegration, functionToStringIntegration, inboundFiltersIntegration, linkedErrorsIntegration, - requestDataIntegration, - metricsDefault as metrics, - startSession, - captureSession, - endSession, -} from '@sentry/core'; - -export { + addEventProcessor, + setContext, + setExtra, + setExtras, + setTag, + setTags, + setUser, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/core'; - -export { autoDiscoverNodePerformanceMonitoringIntegrations } from './tracing'; - -export { NodeClient } from './client'; -export { makeNodeTransport } from './transports'; -export { - getDefaultIntegrations, - init, - defaultStackParser, - getSentryRelease, -} from './sdk'; -export { addRequestDataToEvent, DEFAULT_USER_INCLUDES, extractRequestData } from '@sentry/utils'; - -export { createGetModuleFromFilename } from './module'; - -import * as Handlers from './handlers'; -import * as NodeIntegrations from './integrations'; -import * as TracingIntegrations from './tracing/integrations'; - -// TODO: Deprecate this once we migrated tracing integrations -export const Integrations = { - ...NodeIntegrations, - ...TracingIntegrations, -}; - -export { + setCurrentClient, + Scope, + setMeasurement, + getSpanDescendants, + parameterize, + getClient, + // eslint-disable-next-line deprecation/deprecation + getCurrentHub, + getCurrentScope, + getIsolationScope, + withScope, + withIsolationScope, + captureException, + captureEvent, + captureMessage, captureConsoleIntegration, debugIntegration, dedupeIntegration, extraErrorDataIntegration, rewriteFramesIntegration, sessionTimingIntegration, + metricsDefault as metrics, + startSession, + captureSession, + endSession, + addIntegration, + startSpan, + startSpanManual, + startInactiveSpan, + getActiveSpan, + withActiveSpan, + getRootSpan, + spanToJSON, + trpcMiddleware, } from '@sentry/core'; -export { consoleIntegration } from './integrations/console'; -export { onUncaughtExceptionIntegration } from './integrations/onuncaughtexception'; -export { onUnhandledRejectionIntegration } from './integrations/onunhandledrejection'; -export { modulesIntegration } from './integrations/modules'; -export { contextLinesIntegration } from './integrations/contextlines'; -export { nodeContextIntegration } from './integrations/context'; -export { localVariablesIntegration } from './integrations/local-variables'; -export { spotlightIntegration } from './integrations/spotlight'; -export { anrIntegration } from './integrations/anr'; -export { hapiIntegration } from './integrations/hapi'; -// eslint-disable-next-line deprecation/deprecation -export { Undici, nativeNodeFetchintegration } from './integrations/undici'; -// eslint-disable-next-line deprecation/deprecation -export { Http, httpIntegration } from './integrations/http'; - -// TODO(v8): Remove all of these exports. They were part of a hotfix #10339 where we produced wrong .d.ts files because we were packing packages inside the /build folder. -export type { LocalVariablesIntegrationOptions } from './integrations/local-variables/common'; -export type { DebugSession } from './integrations/local-variables/local-variables-sync'; -export type { AnrIntegrationOptions } from './integrations/anr/common'; -// --- - -export { Handlers }; - -export { hapiErrorPlugin } from './integrations/hapi'; - -import { instrumentCron } from './cron/cron'; -import { instrumentNodeCron } from './cron/node-cron'; -import { instrumentNodeSchedule } from './cron/node-schedule'; - -/** Methods to instrument cron libraries for Sentry check-ins */ -export const cron = { - instrumentCron, - instrumentNodeCron, - instrumentNodeSchedule, -}; +export type { + Breadcrumb, + BreadcrumbHint, + PolymorphicRequest, + Request, + SdkInfo, + Event, + EventHint, + Exception, + Session, + SeverityLevel, + StackFrame, + Stacktrace, + Thread, + User, + Span, +} from '@sentry/types'; diff --git a/packages/node/src/integrations/anr/common.ts b/packages/node/src/integrations/anr/common.ts index 5617871ccb24..e2e50fae4179 100644 --- a/packages/node/src/integrations/anr/common.ts +++ b/packages/node/src/integrations/anr/common.ts @@ -37,6 +37,7 @@ export interface WorkerStartData extends AnrIntegrationOptions { debug: boolean; sdkMetadata: SdkMetadata; dsn: DsnComponents; + tunnel: string | undefined; release: string | undefined; environment: string; dist: string | undefined; diff --git a/packages/node/src/integrations/anr/index.ts b/packages/node/src/integrations/anr/index.ts index 7e0de1d0badc..7dbe9e905cb4 100644 --- a/packages/node/src/integrations/anr/index.ts +++ b/packages/node/src/integrations/anr/index.ts @@ -1,37 +1,41 @@ -// TODO (v8): This import can be removed once we only support Node with global URL -import { URL } from 'url'; -import { defineIntegration, getCurrentScope } from '@sentry/core'; -import type { Contexts, Event, EventHint, IntegrationFn } from '@sentry/types'; -import { dynamicRequire, logger } from '@sentry/utils'; -import type { Worker, WorkerOptions } from 'worker_threads'; -import type { NodeClient } from '../../client'; +import { defineIntegration, mergeScopeData } from '@sentry/core'; +import type { Contexts, Event, EventHint, Integration, IntegrationFn, ScopeData } from '@sentry/types'; +import { GLOBAL_OBJ, logger } from '@sentry/utils'; +import * as inspector from 'inspector'; +import { Worker } from 'worker_threads'; +import { getCurrentScope, getGlobalScope, getIsolationScope } from '../..'; import { NODE_VERSION } from '../../nodeVersion'; +import type { NodeClient } from '../../sdk/client'; import type { AnrIntegrationOptions, WorkerStartData } from './common'; import { base64WorkerScript } from './worker-script'; const DEFAULT_INTERVAL = 50; const DEFAULT_HANG_THRESHOLD = 5000; -type WorkerNodeV14 = Worker & { new (filename: string | URL, options?: WorkerOptions): Worker }; - -type WorkerThreads = { - Worker: WorkerNodeV14; -}; - function log(message: string, ...args: unknown[]): void { logger.log(`[ANR] ${message}`, ...args); } -/** - * We need to use dynamicRequire because worker_threads is not available in node < v12 and webpack error will when - * targeting those versions - */ -function getWorkerThreads(): WorkerThreads { - return dynamicRequire(module, 'worker_threads'); +function globalWithScopeFetchFn(): typeof GLOBAL_OBJ & { __SENTRY_GET_SCOPES__?: () => ScopeData } { + return GLOBAL_OBJ; +} + +/** Fetches merged scope data */ +function getScopeData(): ScopeData { + const scope = getGlobalScope().getScopeData(); + mergeScopeData(scope, getIsolationScope().getScopeData()); + mergeScopeData(scope, getCurrentScope().getScopeData()); + + // We remove attachments because they likely won't serialize well as json + scope.attachments = []; + // We can't serialize event processor functions + scope.eventProcessors = []; + + return scope; } /** - * Gets contexts by calling all event processors. This relies on being called after all integrations are setup + * Gets contexts by calling all event processors. This shouldn't be called until all integrations are setup */ async function getContexts(client: NodeClient): Promise { let event: Event | null = { message: 'ANR' }; @@ -45,40 +49,76 @@ async function getContexts(client: NodeClient): Promise { return event?.contexts || {}; } -interface InspectorApi { - open: (port: number) => void; - url: () => string | undefined; -} - const INTEGRATION_NAME = 'Anr'; +type AnrInternal = { startWorker: () => void; stopWorker: () => void }; + const _anrIntegration = ((options: Partial = {}) => { + if (NODE_VERSION.major < 16 || (NODE_VERSION.major === 16 && NODE_VERSION.minor < 17)) { + throw new Error('ANR detection requires Node 16.17.0 or later'); + } + + let worker: Promise<() => void> | undefined; + let client: NodeClient | undefined; + + // Hookup the scope fetch function to the global object so that it can be called from the worker thread via the + // debugger when it pauses + const gbl = globalWithScopeFetchFn(); + gbl.__SENTRY_GET_SCOPES__ = getScopeData; + return { name: INTEGRATION_NAME, - setup(client: NodeClient) { - if (NODE_VERSION.major < 16 || (NODE_VERSION.major === 16 && NODE_VERSION.minor < 17)) { - throw new Error('ANR detection requires Node 16.17.0 or later'); + startWorker: () => { + if (worker) { + return; + } + + if (client) { + worker = _startWorker(client, options); + } + }, + stopWorker: () => { + if (worker) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + worker.then(stop => { + stop(); + worker = undefined; + }); } + }, + setup(initClient: NodeClient) { + client = initClient; - // setImmediate is used to ensure that all other integrations have been setup - setImmediate(() => _startWorker(client, options)); + // setImmediate is used to ensure that all other integrations have had their setup called first. + // This allows us to call into all integrations to fetch the full context + setImmediate(() => this.startWorker()); }, - }; + } as Integration & AnrInternal; }) satisfies IntegrationFn; -export const anrIntegration = defineIntegration(_anrIntegration); +type AnrReturn = (options?: Partial) => Integration & AnrInternal; + +export const anrIntegration = defineIntegration(_anrIntegration) as AnrReturn; /** * Starts the ANR worker thread + * + * @returns A function to stop the worker */ -async function _startWorker(client: NodeClient, _options: Partial): Promise { - const contexts = await getContexts(client); +async function _startWorker( + client: NodeClient, + integrationOptions: Partial, +): Promise<() => void> { const dsn = client.getDsn(); if (!dsn) { - return; + return () => { + // + }; } + const contexts = await getContexts(client); + // These will not be accurate if sent later from the worker thread delete contexts.app?.app_memory; delete contexts.device?.free_memory; @@ -93,28 +133,25 @@ async function _startWorker(client: NodeClient, _options: Partial { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + worker.terminate(); + clearInterval(timer); + }; } diff --git a/packages/node/src/integrations/anr/worker-script.ts b/packages/node/src/integrations/anr/worker-script.ts index 16394eaacfe1..c70323e0fc50 100644 --- a/packages/node/src/integrations/anr/worker-script.ts +++ b/packages/node/src/integrations/anr/worker-script.ts @@ -1,2 +1,2 @@ -// This file is a placeholder that gets overwritten in the build directory. -export const base64WorkerScript = ''; +// This string is a placeholder that gets overwritten with the worker code. +export const base64WorkerScript = '###base64WorkerScript###'; diff --git a/packages/node/src/integrations/anr/worker.ts b/packages/node/src/integrations/anr/worker.ts index a8b984b48379..21bdcbbb0631 100644 --- a/packages/node/src/integrations/anr/worker.ts +++ b/packages/node/src/integrations/anr/worker.ts @@ -1,11 +1,12 @@ import { + applyScopeDataToEvent, createEventEnvelope, createSessionEnvelope, getEnvelopeEndpointWithUrlEncodedAuth, makeSession, updateSession, } from '@sentry/core'; -import type { Event, Session, StackFrame, TraceContext } from '@sentry/types'; +import type { Event, ScopeData, Session, StackFrame } from '@sentry/types'; import { callFrameToStackFrame, normalizeUrlToBase, @@ -16,12 +17,11 @@ import { import { Session as InspectorSession } from 'inspector'; import { parentPort, workerData } from 'worker_threads'; -import { createGetModuleFromFilename } from '../../module'; import { makeNodeTransport } from '../../transports'; +import { createGetModuleFromFilename } from '../../utils/module'; import type { WorkerStartData } from './common'; type VoidFunction = () => void; -type InspectorSessionNodeV12 = InspectorSession & { connectToMainThread: VoidFunction }; const options: WorkerStartData = workerData; let session: Session | undefined; @@ -34,7 +34,7 @@ function log(msg: string): void { } } -const url = getEnvelopeEndpointWithUrlEncodedAuth(options.dsn); +const url = getEnvelopeEndpointWithUrlEncodedAuth(options.dsn, options.tunnel, options.sdkMetadata.sdk); const transport = makeNodeTransport({ url, recordDroppedEvent: () => { @@ -48,7 +48,7 @@ async function sendAbnormalSession(): Promise { log('Sending abnormal session'); updateSession(session, { status: 'abnormal', abnormal_mechanism: 'anr_foreground' }); - const envelope = createSessionEnvelope(session, options.dsn, options.sdkMetadata); + const envelope = createSessionEnvelope(session, options.dsn, options.sdkMetadata, options.tunnel); // Log the envelope so to aid in testing log(JSON.stringify(envelope)); @@ -87,7 +87,23 @@ function prepareStackFrames(stackFrames: StackFrame[] | undefined): StackFrame[] return strippedFrames; } -async function sendAnrEvent(frames?: StackFrame[], traceContext?: TraceContext): Promise { +function applyScopeToEvent(event: Event, scope: ScopeData): void { + applyScopeDataToEvent(event, scope); + + if (!event.contexts?.trace) { + const { traceId, spanId, parentSpanId } = scope.propagationContext; + event.contexts = { + trace: { + trace_id: traceId, + span_id: spanId, + parent_span_id: parentSpanId, + }, + ...event.contexts, + }; + } +} + +async function sendAnrEvent(frames?: StackFrame[], scope?: ScopeData): Promise { if (hasSentAnrEvent) { return; } @@ -100,7 +116,7 @@ async function sendAnrEvent(frames?: StackFrame[], traceContext?: TraceContext): const event: Event = { event_id: uuid4(), - contexts: { ...options.contexts, trace: traceContext }, + contexts: options.contexts, release: options.release, environment: options.environment, dist: options.dist, @@ -120,15 +136,19 @@ async function sendAnrEvent(frames?: StackFrame[], traceContext?: TraceContext): tags: options.staticTags, }; - const envelope = createEventEnvelope(event, options.dsn, options.sdkMetadata); - // Log the envelope so to aid in testing + if (scope) { + applyScopeToEvent(event, scope); + } + + const envelope = createEventEnvelope(event, options.dsn, options.sdkMetadata, options.tunnel); + // Log the envelope to aid in testing log(JSON.stringify(envelope)); await transport.send(envelope); await transport.flush(2000); - // Delay for 5 seconds so that stdio can flush in the main event loop ever restarts. - // This is mainly for the benefit of logging/debugging issues. + // Delay for 5 seconds so that stdio can flush if the main event loop ever restarts. + // This is mainly for the benefit of logging or debugging. setTimeout(() => { process.exit(0); }, 5_000); @@ -139,7 +159,7 @@ let debuggerPause: VoidFunction | undefined; if (options.captureStackTrace) { log('Connecting to debugger'); - const session = new InspectorSession() as InspectorSessionNodeV12; + const session = new InspectorSession(); session.connectToMainThread(); log('Connected to debugger'); @@ -172,20 +192,23 @@ if (options.captureStackTrace) { 'Runtime.evaluate', { // Grab the trace context from the current scope - expression: - 'const ctx = __SENTRY__.acs?.getCurrentScope().getPropagationContext() || {}; ctx.traceId + "-" + ctx.spanId + "-" + ctx.parentSpanId', + expression: 'global.__SENTRY_GET_SCOPES__();', // Don't re-trigger the debugger if this causes an error silent: true, + // Serialize the result to json otherwise only primitives are supported + returnByValue: true, }, - (_, param) => { - const traceId = param && param.result ? (param.result.value as string) : '--'; - const [trace_id, span_id, parent_span_id] = traceId.split('-') as (string | undefined)[]; + (err, param) => { + if (err) { + log(`Error executing script: '${err.message}'`); + } + + const scopes = param && param.result ? (param.result.value as ScopeData) : undefined; session.post('Debugger.resume'); session.post('Debugger.disable'); - const context = trace_id?.length && span_id?.length ? { trace_id, span_id, parent_span_id } : undefined; - sendAnrEvent(stackFrames, context).then(null, () => { + sendAnrEvent(stackFrames, scopes).then(null, () => { log('Sending ANR event failed.'); }); }, diff --git a/packages/node/src/integrations/console.ts b/packages/node/src/integrations/console.ts index 5a185b8fcee1..0b3d27fe8510 100644 --- a/packages/node/src/integrations/console.ts +++ b/packages/node/src/integrations/console.ts @@ -1,6 +1,6 @@ import * as util from 'util'; -import { addBreadcrumb, convertIntegrationFnToClass, defineIntegration, getClient } from '@sentry/core'; -import type { Client, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; +import { addBreadcrumb, defineIntegration, getClient } from '@sentry/core'; +import type { IntegrationFn } from '@sentry/types'; import { addConsoleInstrumentationHandler, severityLevelFromString } from '@sentry/utils'; const INTEGRATION_NAME = 'Console'; @@ -30,16 +30,7 @@ const _consoleIntegration = (() => { }; }) satisfies IntegrationFn; -export const consoleIntegration = defineIntegration(_consoleIntegration); - /** - * Console module integration. - * @deprecated Use `consoleIntegration()` instead. + * Capture console logs as breadcrumbs. */ -// eslint-disable-next-line deprecation/deprecation -export const Console = convertIntegrationFnToClass(INTEGRATION_NAME, consoleIntegration) as IntegrationClass< - Integration & { setup: (client: Client) => void } ->; - -// eslint-disable-next-line deprecation/deprecation -export type Console = typeof Console; +export const consoleIntegration = defineIntegration(_consoleIntegration); diff --git a/packages/node/src/integrations/context.ts b/packages/node/src/integrations/context.ts index fa5184204bf2..c33d97e79044 100644 --- a/packages/node/src/integrations/context.ts +++ b/packages/node/src/integrations/context.ts @@ -1,10 +1,9 @@ -/* eslint-disable max-lines */ import { execFile } from 'child_process'; import { readFile, readdir } from 'fs'; import * as os from 'os'; import { join } from 'path'; import { promisify } from 'util'; -import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; +import { defineIntegration } from '@sentry/core'; import type { AppContext, CloudResourceContext, @@ -12,13 +11,10 @@ import type { CultureContext, DeviceContext, Event, - Integration, - IntegrationClass, IntegrationFn, OsContext, } from '@sentry/types'; -// TODO: Required until we drop support for Node v8 export const readFileAsync = promisify(readFile); export const readDirAsync = promisify(readdir); @@ -108,27 +104,10 @@ const _nodeContextIntegration = ((options: ContextOptions = {}) => { }; }) satisfies IntegrationFn; -export const nodeContextIntegration = defineIntegration(_nodeContextIntegration); - /** - * Add node modules / packages to the event. - * @deprecated Use `nodeContextIntegration()` instead. + * Capture context about the environment and the device that the client is running on, to events. */ -// eslint-disable-next-line deprecation/deprecation -export const Context = convertIntegrationFnToClass(INTEGRATION_NAME, nodeContextIntegration) as IntegrationClass< - Integration & { processEvent: (event: Event) => Promise } -> & { - new (options?: { - app?: boolean; - os?: boolean; - device?: { cpu?: boolean; memory?: boolean } | boolean; - culture?: boolean; - cloudResource?: boolean; - }): Integration; -}; - -// eslint-disable-next-line deprecation/deprecation -export type Context = typeof Context; +export const nodeContextIntegration = defineIntegration(_nodeContextIntegration); /** * Updates the context with dynamic values that can change diff --git a/packages/node/src/integrations/contextlines.ts b/packages/node/src/integrations/contextlines.ts index 7c367f51a970..3755e164e5ea 100644 --- a/packages/node/src/integrations/contextlines.ts +++ b/packages/node/src/integrations/contextlines.ts @@ -1,4 +1,4 @@ -import { readFile } from 'fs'; +import { promises } from 'fs'; import { defineIntegration } from '@sentry/core'; import type { Event, IntegrationFn, StackFrame } from '@sentry/types'; import { LRUMap, addContextToFrame } from '@sentry/utils'; @@ -7,15 +7,7 @@ const FILE_CONTENT_CACHE = new LRUMap(100); const DEFAULT_LINES_OF_CONTEXT = 7; const INTEGRATION_NAME = 'ContextLines'; -// TODO: Replace with promisify when minimum supported node >= v8 -function readTextFileAsync(path: string): Promise { - return new Promise((resolve, reject) => { - readFile(path, 'utf8', (err, data) => { - if (err) reject(err); - else resolve(data); - }); - }); -} +const readFileAsync = promises.readFile; /** * Resets the file cache. Exists for testing purposes. @@ -35,7 +27,8 @@ interface ContextLinesOptions { frameContextLines?: number; } -const _contextLinesIntegration = ((options: ContextLinesOptions = {}) => { +/** Exported only for tests, as a type-safe variant. */ +export const _contextLinesIntegration = ((options: ContextLinesOptions = {}) => { const contextLines = options.frameContextLines !== undefined ? options.frameContextLines : DEFAULT_LINES_OF_CONTEXT; return { @@ -46,6 +39,9 @@ const _contextLinesIntegration = ((options: ContextLinesOptions = {}) => { }; }) satisfies IntegrationFn; +/** + * Capture the lines before and after the frame's context. + */ export const contextLinesIntegration = defineIntegration(_contextLinesIntegration); async function addSourceContext(event: Event, contextLines: number): Promise { @@ -139,7 +135,7 @@ async function _readSourceFile(filename: string): Promise { // If we made it to here, it means that our file is not cache nor marked as failed, so attempt to read it let content: string[] | null = null; try { - const rawFileContents = await readTextFileAsync(filename); + const rawFileContents = await readFileAsync(filename, 'utf-8'); content = rawFileContents.split('\n'); } catch (_) { // if we fail, we will mark the file as null in the cache and short circuit next time we try to read it diff --git a/packages/node/src/integrations/hapi/index.ts b/packages/node/src/integrations/hapi/index.ts deleted file mode 100644 index 894eca0748fe..000000000000 --- a/packages/node/src/integrations/hapi/index.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { - SDK_VERSION, - SPAN_STATUS_ERROR, - captureException, - continueTrace, - convertIntegrationFnToClass, - defineIntegration, - getActiveSpan, - getCurrentScope, - getDynamicSamplingContextFromSpan, - getRootSpan, - setHttpStatus, - spanToTraceHeader, - startInactiveSpan, -} from '@sentry/core'; - -import type { IntegrationFn } from '@sentry/types'; -import { dynamicSamplingContextToSentryBaggageHeader, fill } from '@sentry/utils'; -import { _setSpanForScope } from '../../_setSpanForScope'; - -import type { Boom, RequestEvent, ResponseObject, Server } from './types'; - -function isResponseObject(response: ResponseObject | Boom): response is ResponseObject { - return response && (response as ResponseObject).statusCode !== undefined; -} - -function isErrorEvent(event: RequestEvent): event is RequestEvent { - return event && (event as RequestEvent).error !== undefined; -} - -function sendErrorToSentry(errorData: object): void { - captureException(errorData, { - mechanism: { - type: 'hapi', - handled: false, - data: { - function: 'hapiErrorPlugin', - }, - }, - }); -} - -export const hapiErrorPlugin = { - name: 'SentryHapiErrorPlugin', - version: SDK_VERSION, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - register: async function (serverArg: Record) { - const server = serverArg as unknown as Server; - - server.events.on('request', (request, event) => { - const activeSpan = getActiveSpan(); - const rootSpan = activeSpan && getRootSpan(activeSpan); - - if (isErrorEvent(event)) { - sendErrorToSentry(event.error); - } - - if (rootSpan) { - rootSpan.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); - rootSpan.end(); - } - }); - }, -}; - -export const hapiTracingPlugin = { - name: 'SentryHapiTracingPlugin', - version: SDK_VERSION, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - register: async function (serverArg: Record) { - const server = serverArg as unknown as Server; - - server.ext('onPreHandler', (request, h) => { - const transaction = continueTrace( - { - sentryTrace: request.headers['sentry-trace'] || undefined, - baggage: request.headers['baggage'] || undefined, - }, - () => { - return startInactiveSpan({ - op: 'hapi.request', - name: `${request.route.method} ${request.path}`, - forceTransaction: true, - }); - }, - ); - - _setSpanForScope(getCurrentScope(), transaction); - - return h.continue; - }); - - server.ext('onPreResponse', (request, h) => { - const activeSpan = getActiveSpan(); - const rootSpan = activeSpan && getRootSpan(activeSpan); - - if (request.response && isResponseObject(request.response) && rootSpan) { - const response = request.response as ResponseObject; - response.header('sentry-trace', spanToTraceHeader(rootSpan)); - - const dynamicSamplingContext = dynamicSamplingContextToSentryBaggageHeader( - getDynamicSamplingContextFromSpan(rootSpan), - ); - - if (dynamicSamplingContext) { - response.header('baggage', dynamicSamplingContext); - } - } - - return h.continue; - }); - - server.ext('onPostHandler', (request, h) => { - const activeSpan = getActiveSpan(); - const rootSpan = activeSpan && getRootSpan(activeSpan); - - if (rootSpan) { - if (request.response && isResponseObject(request.response)) { - setHttpStatus(rootSpan, request.response.statusCode); - } - - rootSpan.end(); - } - - return h.continue; - }); - }, -}; - -export type HapiOptions = { - /** Hapi server instance */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - server?: Record; -}; - -const INTEGRATION_NAME = 'Hapi'; - -const _hapiIntegration = ((options: HapiOptions = {}) => { - const server = options.server as undefined | Server; - - return { - name: INTEGRATION_NAME, - setupOnce() { - if (!server) { - return; - } - - fill(server, 'start', (originalStart: () => void) => { - return async function (this: Server) { - await this.register(hapiTracingPlugin); - await this.register(hapiErrorPlugin); - const result = originalStart.apply(this); - return result; - }; - }); - }, - }; -}) satisfies IntegrationFn; - -export const hapiIntegration = defineIntegration(_hapiIntegration); - -/** - * Hapi Framework Integration. - * @deprecated Use `hapiIntegration()` instead. - */ -// eslint-disable-next-line deprecation/deprecation -export const Hapi = convertIntegrationFnToClass(INTEGRATION_NAME, hapiIntegration); - -// eslint-disable-next-line deprecation/deprecation -export type Hapi = typeof Hapi; diff --git a/packages/node/src/integrations/hapi/types.ts b/packages/node/src/integrations/hapi/types.ts deleted file mode 100644 index a650667fe362..000000000000 --- a/packages/node/src/integrations/hapi/types.ts +++ /dev/null @@ -1,279 +0,0 @@ -/* eslint-disable @typescript-eslint/no-misused-new */ -/* eslint-disable @typescript-eslint/naming-convention */ -/* eslint-disable @typescript-eslint/unified-signatures */ -/* eslint-disable @typescript-eslint/no-empty-interface */ -/* eslint-disable @typescript-eslint/no-namespace */ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -// Vendored and simplified from: -// - @types/hapi__hapi -// v17.8.9999 -// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/c73060bd14bb74a2f1906ccfc714d385863bc07d/types/hapi/v17/index.d.ts -// -// - @types/podium -// v1.0.9999 -// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/c73060bd14bb74a2f1906ccfc714d385863bc07d/types/podium/index.d.ts -// -// - @types/boom -// v7.3.9999 -// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/c73060bd14bb74a2f1906ccfc714d385863bc07d/types/boom/v4/index.d.ts - -import type * as stream from 'stream'; -import type * as url from 'url'; - -interface Podium { - new (events?: Events[]): Podium; - new (events?: Events): Podium; - - registerEvent(events: Events[]): void; - registerEvent(events: Events): void; - - registerPodium?(podiums: Podium[]): void; - registerPodium?(podiums: Podium): void; - - emit( - criteria: string | { name: string; channel?: string | undefined; tags?: string | string[] | undefined }, - data: any, - callback?: () => void, - ): void; - - on(criteria: string | Criteria, listener: Listener): void; - addListener(criteria: string | Criteria, listener: Listener): void; - once(criteria: string | Criteria, listener: Listener): void; - removeListener(name: string, listener: Listener): Podium; - removeAllListeners(name: string): Podium; - hasListeners(name: string): boolean; -} - -export interface Boom extends Error { - isBoom: boolean; - isServer: boolean; - message: string; - output: Output; - reformat: () => string; - isMissing?: boolean | undefined; - data: Data; -} - -export interface Output { - statusCode: number; - headers: { [index: string]: string }; - payload: Payload; -} - -export interface Payload { - statusCode: number; - error: string; - message: string; - attributes?: any; -} - -export type Events = string | EventOptionsObject | Podium; - -export interface EventOptionsObject { - name: string; - channels?: string | string[] | undefined; - clone?: boolean | undefined; - spread?: boolean | undefined; - tags?: boolean | undefined; - shared?: boolean | undefined; -} - -export interface CriteriaObject { - name: string; - block?: boolean | number | undefined; - channels?: string | string[] | undefined; - clone?: boolean | undefined; - count?: number | undefined; - filter?: string | string[] | CriteriaFilterOptionsObject | undefined; - spread?: boolean | undefined; - tags?: boolean | undefined; - listener?: Listener | undefined; -} - -export interface CriteriaFilterOptionsObject { - tags?: string | string[] | undefined; - all?: boolean | undefined; -} - -export type Criteria = string | CriteriaObject; - -export interface Listener { - (data: any, tags?: Tags, callback?: () => void): void; -} - -export type Tags = { [tag: string]: boolean }; - -type Dependencies = - | string - | string[] - | { - [key: string]: string; - }; - -interface PluginNameVersion { - name: string; - version?: string | undefined; -} - -interface PluginPackage { - pkg: any; -} - -interface PluginBase { - register: (server: Server, options: T) => void | Promise; - multiple?: boolean | undefined; - dependencies?: Dependencies | undefined; - requirements?: - | { - node?: string | undefined; - hapi?: string | undefined; - } - | undefined; - - once?: boolean | undefined; -} - -type Plugin = PluginBase & (PluginNameVersion | PluginPackage); - -interface UserCredentials {} - -interface AppCredentials {} - -interface AuthCredentials { - scope?: string[] | undefined; - user?: UserCredentials | undefined; - app?: AppCredentials | undefined; -} - -interface RequestAuth { - artifacts: object; - credentials: AuthCredentials; - error: Error; - isAuthenticated: boolean; - isAuthorized: boolean; - mode: string; - strategy: string; -} - -interface RequestEvents extends Podium { - on(criteria: 'peek', listener: PeekListener): void; - on(criteria: 'finish' | 'disconnect', listener: (data: undefined) => void): void; - once(criteria: 'peek', listener: PeekListener): void; - once(criteria: 'finish' | 'disconnect', listener: (data: undefined) => void): void; -} - -namespace Lifecycle { - export type Method = (request: Request, h: ResponseToolkit, err?: Error) => ReturnValue; - export type ReturnValue = ReturnValueTypes | Promise; - export type ReturnValueTypes = - | (null | string | number | boolean) - | Buffer - | (Error | Boom) - | stream.Stream - | (object | object[]) - | symbol - | ResponseToolkit; - export type FailAction = 'error' | 'log' | 'ignore' | Method; -} - -namespace Util { - export interface Dictionary { - [key: string]: T; - } - - export type HTTP_METHODS_PARTIAL_LOWERCASE = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'options'; - export type HTTP_METHODS_PARTIAL = - | 'GET' - | 'POST' - | 'PUT' - | 'PATCH' - | 'DELETE' - | 'OPTIONS' - | HTTP_METHODS_PARTIAL_LOWERCASE; - export type HTTP_METHODS = 'HEAD' | 'head' | HTTP_METHODS_PARTIAL; -} - -interface RequestRoute { - method: Util.HTTP_METHODS_PARTIAL; - path: string; - vhost?: string | string[] | undefined; - realm: any; - fingerprint: string; - - auth: { - access(request: Request): boolean; - }; -} - -interface Request extends Podium { - app: ApplicationState; - readonly auth: RequestAuth; - events: RequestEvents; - readonly headers: Util.Dictionary; - readonly path: string; - response: ResponseObject | Boom | null; - readonly route: RequestRoute; - readonly url: url.Url; -} - -interface ResponseObjectHeaderOptions { - append?: boolean | undefined; - separator?: string | undefined; - override?: boolean | undefined; - duplicate?: boolean | undefined; -} - -export interface ResponseObject extends Podium { - readonly statusCode: number; - header(name: string, value: string, options?: ResponseObjectHeaderOptions): ResponseObject; -} - -interface ResponseToolkit { - readonly continue: symbol; -} - -interface ServerEventCriteria { - name: T; - channels?: string | string[] | undefined; - clone?: boolean | undefined; - count?: number | undefined; - filter?: string | string[] | { tags: string | string[]; all?: boolean | undefined } | undefined; - spread?: boolean | undefined; - tags?: boolean | undefined; -} - -export interface RequestEvent { - timestamp: string; - tags: string[]; - channel: 'internal' | 'app' | 'error'; - data: object; - error: object; -} - -type RequestEventHandler = (request: Request, event: RequestEvent, tags: { [key: string]: true }) => void; -interface ServerEvents { - on(criteria: 'request' | ServerEventCriteria<'request'>, listener: RequestEventHandler): void; -} - -type RouteRequestExtType = - | 'onPreAuth' - | 'onCredentials' - | 'onPostAuth' - | 'onPreHandler' - | 'onPostHandler' - | 'onPreResponse'; - -type ServerRequestExtType = RouteRequestExtType | 'onRequest'; - -export type Server = Record & { - events: ServerEvents; - ext(event: ServerRequestExtType, method: Lifecycle.Method, options?: Record): void; - initialize(): Promise; - register(plugins: Plugin | Array>, options?: Record): Promise; - start(): Promise; -}; - -interface ApplicationState {} - -type PeekListener = (chunk: string, encoding: string) => void; diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index 9eb7deab5318..ed93ebacdaa5 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -1,468 +1,155 @@ -/* eslint-disable max-lines */ -import type * as http from 'http'; -import type * as https from 'https'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; -import { defineIntegration, getIsolationScope, hasTracingEnabled } from '@sentry/core'; -import { - addBreadcrumb, - getClient, - getCurrentScope, - getDynamicSamplingContextFromClient, - getDynamicSamplingContextFromSpan, - isSentryRequestUrl, - setHttpStatus, - spanToJSON, - spanToTraceHeader, -} from '@sentry/core'; -import type { - ClientOptions, - Integration, - IntegrationFn, - SanitizedRequestData, - TracePropagationTargets, -} from '@sentry/types'; -import { - LRUMap, - dropUndefinedKeys, - dynamicSamplingContextToSentryBaggageHeader, - fill, - generateSentryTraceHeader, - logger, - stringMatchesSomePattern, -} from '@sentry/utils'; - -import type { NodeClient } from '../client'; -import { DEBUG_BUILD } from '../debug-build'; -import { NODE_VERSION } from '../nodeVersion'; -import type { NodeClientOptions } from '../types'; -import type { RequestMethod, RequestMethodArgs, RequestOptions } from './utils/http'; -import { cleanSpanName, extractRawUrl, extractUrl, normalizeRequestArgs } from './utils/http'; - -interface TracingOptions { - /** - * List of strings/regex controlling to which outgoing requests - * the SDK will attach tracing headers. - * - * By default the SDK will attach those headers to all outgoing - * requests. If this option is provided, the SDK will match the - * request URL of outgoing requests against the items in this - * array, and only attach tracing headers if a match was found. - * - * @deprecated Use top level `tracePropagationTargets` option instead. - * This option will be removed in v8. - * - * ``` - * Sentry.init({ - * tracePropagationTargets: ['api.site.com'], - * }) - */ - tracePropagationTargets?: TracePropagationTargets; - - /** - * Function determining whether or not to create spans to track outgoing requests to the given URL. - * By default, spans will be created for all outgoing requests. - */ - shouldCreateSpanForRequest?: (url: string) => boolean; - - /** - * This option is just for compatibility with v7. - * In v8, this will be the default behavior. - */ - enableIfHasTracingEnabled?: boolean; -} +import type { ServerResponse } from 'http'; +import type { Span } from '@opentelemetry/api'; +import { SpanKind } from '@opentelemetry/api'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; +import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; + +import { addBreadcrumb, defineIntegration, getIsolationScope, isSentryRequestUrl } from '@sentry/core'; +import { _INTERNAL, getClient, getSpanKind } from '@sentry/opentelemetry'; +import type { IntegrationFn } from '@sentry/types'; + +import type { NodeClient } from '../sdk/client'; +import { setIsolationScope } from '../sdk/scope'; +import type { HTTPModuleRequestIncomingMessage } from '../transports/http-module'; +import { addOriginToSpan } from '../utils/addOriginToSpan'; +import { getRequestUrl } from '../utils/getRequestUrl'; interface HttpOptions { /** - * Whether breadcrumbs should be recorded for requests + * Whether breadcrumbs should be recorded for requests. * Defaults to true */ breadcrumbs?: boolean; /** - * Whether tracing spans should be created for requests - * Defaults to false + * 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. */ - tracing?: TracingOptions | boolean; -} + ignoreOutgoingRequests?: (url: string) => boolean; -/* These are the newer options for `httpIntegration`. */ -interface HttpIntegrationOptions { /** - * Whether breadcrumbs should be recorded for requests - * Defaults to true. + * Do not capture spans or breadcrumbs for incoming 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. */ - breadcrumbs?: boolean; - - /** - * Whether tracing spans should be created for requests - * If not set, this will be enabled/disabled based on if tracing is enabled. - */ - tracing?: boolean; - - /** - * Function determining whether or not to create spans to track outgoing requests to the given URL. - * By default, spans will be created for all outgoing requests. - */ - shouldCreateSpanForRequest?: (url: string) => boolean; + ignoreIncomingRequests?: (url: string) => boolean; } -const _httpIntegration = ((options: HttpIntegrationOptions = {}) => { - const { breadcrumbs, tracing, shouldCreateSpanForRequest } = options; - - const convertedOptions: HttpOptions = { - breadcrumbs, - tracing: - tracing === false - ? false - : dropUndefinedKeys({ - // If tracing is forced to `true`, we don't want to set `enableIfHasTracingEnabled` - enableIfHasTracingEnabled: tracing === true ? undefined : true, - shouldCreateSpanForRequest, - }), - }; - // eslint-disable-next-line deprecation/deprecation - return new Http(convertedOptions) as unknown as Integration; -}) satisfies IntegrationFn; - -/** - * The http module integration instruments Node's internal http module. It creates breadcrumbs, spans for outgoing - * http requests, and attaches trace data when tracing is enabled via its `tracing` option. - * - * By default, this will always create breadcrumbs, and will create spans if tracing is enabled. - */ -export const httpIntegration = defineIntegration(_httpIntegration); - -/** - * The http integration instruments Node's internal http and https modules. - * It creates breadcrumbs and spans for outgoing HTTP requests which will be attached to the currently active span. - * - * @deprecated Use `httpIntegration()` instead. - */ -export class Http implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'Http'; +const _httpIntegration = ((options: HttpOptions = {}) => { + const _breadcrumbs = typeof options.breadcrumbs === 'undefined' ? true : options.breadcrumbs; + const _ignoreOutgoingRequests = options.ignoreOutgoingRequests; + const _ignoreIncomingRequests = options.ignoreIncomingRequests; + + return { + name: 'Http', + setupOnce() { + const instrumentations = [ + new HttpInstrumentation({ + ignoreOutgoingRequestHook: request => { + const url = getRequestUrl(request); + + if (!url) { + return false; + } - /** - * @inheritDoc - */ - // eslint-disable-next-line deprecation/deprecation - public name: string = Http.id; + if (isSentryRequestUrl(url, getClient())) { + return true; + } - private readonly _breadcrumbs: boolean; - private readonly _tracing: TracingOptions | undefined; + if (_ignoreOutgoingRequests && _ignoreOutgoingRequests(url)) { + return true; + } - /** - * @inheritDoc - */ - public constructor(options: HttpOptions = {}) { - this._breadcrumbs = typeof options.breadcrumbs === 'undefined' ? true : options.breadcrumbs; - this._tracing = !options.tracing ? undefined : options.tracing === true ? {} : options.tracing; - } + return false; + }, - /** - * @inheritDoc - */ - public setupOnce(): void { - const clientOptions = getClient()?.getOptions(); + ignoreIncomingRequestHook: request => { + const url = getRequestUrl(request); - // If `tracing` is not explicitly set, we default this based on whether or not tracing is enabled. - // But for compatibility, we only do that if `enableIfHasTracingEnabled` is set. - const shouldCreateSpans = _shouldCreateSpans(this._tracing, clientOptions); + const method = request.method?.toUpperCase(); + // We do not capture OPTIONS/HEAD requests as transactions + if (method === 'OPTIONS' || method === 'HEAD') { + return true; + } - // No need to instrument if we don't want to track anything - if (!this._breadcrumbs && !shouldCreateSpans) { - return; - } + if (_ignoreIncomingRequests && _ignoreIncomingRequests(url)) { + return true; + } - const shouldCreateSpanForRequest = _getShouldCreateSpanForRequest(shouldCreateSpans, this._tracing, clientOptions); + return false; + }, - // eslint-disable-next-line deprecation/deprecation - const tracePropagationTargets = clientOptions?.tracePropagationTargets || this._tracing?.tracePropagationTargets; + requireParentforOutgoingSpans: true, + requireParentforIncomingSpans: false, + requestHook: (span, req) => { + _updateSpan(span); - // eslint-disable-next-line @typescript-eslint/no-var-requires - const httpModule = require('http'); - const wrappedHttpHandlerMaker = _createWrappedRequestMethodFactory( - httpModule, - this._breadcrumbs, - shouldCreateSpanForRequest, - tracePropagationTargets, - ); - fill(httpModule, 'get', wrappedHttpHandlerMaker); - fill(httpModule, 'request', wrappedHttpHandlerMaker); + // Update the isolation scope, isolate this request + if (getSpanKind(span) === SpanKind.SERVER) { + const isolationScope = getIsolationScope().clone(); + isolationScope.setSDKProcessingMetadata({ request: req }); - // NOTE: Prior to Node 9, `https` used internals of `http` module, thus we don't patch it. - // If we do, we'd get double breadcrumbs and double spans for `https` calls. - // It has been changed in Node 9, so for all versions equal and above, we patch `https` separately. - if (NODE_VERSION.major > 8) { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const httpsModule = require('node:https'); - const wrappedHttpsHandlerMaker = _createWrappedRequestMethodFactory( - httpsModule, - this._breadcrumbs, - shouldCreateSpanForRequest, - tracePropagationTargets, - ); - fill(httpsModule, 'get', wrappedHttpsHandlerMaker); - fill(httpsModule, 'request', wrappedHttpsHandlerMaker); - } - } -} + const client = getClient(); + if (client && client.getOptions().autoSessionTracking) { + isolationScope.setRequestSession({ status: 'ok' }); + } + setIsolationScope(isolationScope); + } + }, + responseHook: (span, res) => { + if (_breadcrumbs) { + _addRequestBreadcrumb(span, res); + } -// for ease of reading below -type OriginalRequestMethod = RequestMethod; -type WrappedRequestMethod = RequestMethod; -type WrappedRequestMethodFactory = (original: OriginalRequestMethod) => WrappedRequestMethod; + const client = getClient(); + if (client && client.getOptions().autoSessionTracking) { + setImmediate(() => { + client['_captureRequestSession'](); + }); + } + }, + }), + ]; + + registerInstrumentations({ + instrumentations, + }); + }, + }; +}) satisfies IntegrationFn; /** - * Function which creates a function which creates wrapped versions of internal `request` and `get` calls within `http` - * and `https` modules. (NB: Not a typo - this is a creator^2!) - * - * @param breadcrumbsEnabled Whether or not to record outgoing requests as breadcrumbs - * @param tracingEnabled Whether or not to record outgoing requests as tracing spans - * - * @returns A function which accepts the exiting handler and returns a wrapped handler + * The http integration instruments Node's internal http and https modules. + * It creates breadcrumbs and spans for outgoing HTTP requests which will be attached to the currently active span. */ -function _createWrappedRequestMethodFactory( - httpModule: typeof http | typeof https, - breadcrumbsEnabled: boolean, - shouldCreateSpanForRequest: ((url: string) => boolean) | undefined, - tracePropagationTargets: TracePropagationTargets | undefined, -): WrappedRequestMethodFactory { - // We're caching results so we don't have to recompute regexp every time we create a request. - const createSpanUrlMap = new LRUMap(100); - const headersUrlMap = new LRUMap(100); - - const shouldCreateSpan = (url: string): boolean => { - if (shouldCreateSpanForRequest === undefined) { - return true; - } - - const cachedDecision = createSpanUrlMap.get(url); - if (cachedDecision !== undefined) { - return cachedDecision; - } - - const decision = shouldCreateSpanForRequest(url); - createSpanUrlMap.set(url, decision); - return decision; - }; - - const shouldAttachTraceData = (url: string): boolean => { - if (tracePropagationTargets === undefined) { - return true; - } - - const cachedDecision = headersUrlMap.get(url); - if (cachedDecision !== undefined) { - return cachedDecision; - } - - const decision = stringMatchesSomePattern(url, tracePropagationTargets); - headersUrlMap.set(url, decision); - return decision; - }; - - /** - * Captures Breadcrumb based on provided request/response pair - */ - function addRequestBreadcrumb( - event: string, - requestSpanData: SanitizedRequestData, - req: http.ClientRequest, - res?: http.IncomingMessage, - ): void { - if (!getClient()?.getIntegrationByName('Http')) { - return; - } - - addBreadcrumb( - { - category: 'http', - data: { - status_code: res && res.statusCode, - ...requestSpanData, - }, - type: 'http', - }, - { - event, - request: req, - response: res, - }, - ); - } - - return function wrappedRequestMethodFactory(originalRequestMethod: OriginalRequestMethod): WrappedRequestMethod { - return function wrappedMethod(this: unknown, ...args: RequestMethodArgs): http.ClientRequest { - const requestArgs = normalizeRequestArgs(httpModule, args); - const requestOptions = requestArgs[0]; - const rawRequestUrl = extractRawUrl(requestOptions); - const requestUrl = extractUrl(requestOptions); - const client = getClient(); - - // we don't want to record requests to Sentry as either breadcrumbs or spans, so just use the original method - if (isSentryRequestUrl(requestUrl, client)) { - return originalRequestMethod.apply(httpModule, requestArgs); - } - - const scope = getCurrentScope(); - const isolationScope = getIsolationScope(); - - const attributes = getRequestSpanData(requestUrl, requestOptions); - - const requestSpan = shouldCreateSpan(rawRequestUrl) - ? startInactiveSpan({ - onlyIfParent: true, - op: 'http.client', - name: `${attributes['http.method']} ${attributes.url}`, - attributes: { - ...attributes, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.node.http', - }, - }) - : undefined; - - if (client && shouldAttachTraceData(rawRequestUrl)) { - const { traceId, spanId, sampled, dsc } = { - ...isolationScope.getPropagationContext(), - ...scope.getPropagationContext(), - }; - - const sentryTraceHeader = requestSpan - ? spanToTraceHeader(requestSpan) - : generateSentryTraceHeader(traceId, spanId, sampled); - - const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader( - dsc || - (requestSpan - ? getDynamicSamplingContextFromSpan(requestSpan) - : getDynamicSamplingContextFromClient(traceId, client)), - ); - - addHeadersToRequestOptions(requestOptions, requestUrl, sentryTraceHeader, sentryBaggageHeader); - } else { - DEBUG_BUILD && - logger.log( - `[Tracing] Not adding sentry-trace header to outgoing request (${requestUrl}) due to mismatching tracePropagationTargets option.`, - ); - } - - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - return originalRequestMethod - .apply(httpModule, requestArgs) - .once('response', function (this: http.ClientRequest, res: http.IncomingMessage): void { - // eslint-disable-next-line @typescript-eslint/no-this-alias - const req = this; - if (breadcrumbsEnabled) { - addRequestBreadcrumb('response', attributes, req, res); - } - if (requestSpan) { - if (res.statusCode) { - setHttpStatus(requestSpan, res.statusCode); - } - requestSpan.updateName(cleanSpanName(spanToJSON(requestSpan).description || '', requestOptions, req) || ''); - requestSpan.end(); - } - }) - .once('error', function (this: http.ClientRequest): void { - // eslint-disable-next-line @typescript-eslint/no-this-alias - const req = this; +export const httpIntegration = defineIntegration(_httpIntegration); - if (breadcrumbsEnabled) { - addRequestBreadcrumb('error', attributes, req); - } - if (requestSpan) { - setHttpStatus(requestSpan, 500); - requestSpan.updateName(cleanSpanName(spanToJSON(requestSpan).description || '', requestOptions, req) || ''); - requestSpan.end(); - } - }); - }; - }; +/** Update the span with data we need. */ +function _updateSpan(span: Span): void { + addOriginToSpan(span, 'auto.http.otel.http'); } -function addHeadersToRequestOptions( - requestOptions: RequestOptions, - requestUrl: string, - sentryTraceHeader: string, - sentryBaggageHeader: string | undefined, -): void { - // Don't overwrite sentry-trace and baggage header if it's already set. - const headers = requestOptions.headers || {}; - if (headers['sentry-trace']) { +/** Add a breadcrumb for outgoing requests. */ +function _addRequestBreadcrumb(span: Span, response: HTTPModuleRequestIncomingMessage | ServerResponse): void { + if (getSpanKind(span) !== SpanKind.CLIENT) { return; } - DEBUG_BUILD && - logger.log(`[Tracing] Adding sentry-trace header ${sentryTraceHeader} to outgoing request to "${requestUrl}": `); - - requestOptions.headers = { - ...requestOptions.headers, - 'sentry-trace': sentryTraceHeader, - // Setting a header to `undefined` will crash in node so we only set the baggage header when it's defined - ...(sentryBaggageHeader && - sentryBaggageHeader.length > 0 && { baggage: normalizeBaggageHeader(requestOptions, sentryBaggageHeader) }), - }; -} - -function getRequestSpanData(requestUrl: string, requestOptions: RequestOptions): SanitizedRequestData { - const method = requestOptions.method || 'GET'; - const data: SanitizedRequestData = { - url: requestUrl, - 'http.method': method, - }; - if (requestOptions.hash) { - // strip leading "#" - data['http.fragment'] = requestOptions.hash.substring(1); - } - if (requestOptions.search) { - // strip leading "?" - data['http.query'] = requestOptions.search.substring(1); - } - return data; -} - -function normalizeBaggageHeader( - requestOptions: RequestOptions, - sentryBaggageHeader: string | undefined, -): string | string[] | undefined { - if (!requestOptions.headers || !requestOptions.headers.baggage) { - return sentryBaggageHeader; - } else if (!sentryBaggageHeader) { - return requestOptions.headers.baggage as string | string[]; - } else if (Array.isArray(requestOptions.headers.baggage)) { - return [...requestOptions.headers.baggage, sentryBaggageHeader]; - } - // Type-cast explanation: - // Technically this the following could be of type `(number | string)[]` but for the sake of simplicity - // we say this is undefined behaviour, since it would not be baggage spec conform if the user did this. - return [requestOptions.headers.baggage, sentryBaggageHeader] as string[]; -} - -/** Exported for tests only. */ -export function _shouldCreateSpans( - tracingOptions: TracingOptions | undefined, - clientOptions: Partial | undefined, -): boolean { - return tracingOptions === undefined - ? false - : tracingOptions.enableIfHasTracingEnabled - ? hasTracingEnabled(clientOptions) - : true; -} - -/** Exported for tests only. */ -export function _getShouldCreateSpanForRequest( - shouldCreateSpans: boolean, - tracingOptions: TracingOptions | undefined, - clientOptions: Partial | undefined, -): undefined | ((url: string) => boolean) { - const handler = shouldCreateSpans - ? // eslint-disable-next-line deprecation/deprecation - tracingOptions?.shouldCreateSpanForRequest || clientOptions?.shouldCreateSpanForRequest - : () => false; - - return handler; + const data = _INTERNAL.getRequestSpanData(span); + addBreadcrumb( + { + category: 'http', + data: { + status_code: response.statusCode, + ...data, + }, + type: 'http', + }, + { + event: 'response', + // TODO FN: Do we need access to `request` here? + // If we do, we'll have to use the `applyCustomAttributesOnSpan` hook instead, + // but this has worse context semantics than request/responseHook. + response, + }, + ); } diff --git a/packages/node/src/integrations/index.ts b/packages/node/src/integrations/index.ts deleted file mode 100644 index 7efc74627e99..000000000000 --- a/packages/node/src/integrations/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable deprecation/deprecation */ -export { Console } from './console'; -export { Http } from './http'; -export { OnUncaughtException } from './onuncaughtexception'; -export { OnUnhandledRejection } from './onunhandledrejection'; -export { Modules } from './modules'; -export { Context } from './context'; -export { Undici } from './undici'; -export { Spotlight } from './spotlight'; -export { Hapi } from './hapi'; diff --git a/packages/node/src/integrations/local-variables/inspector.d.ts b/packages/node/src/integrations/local-variables/inspector.d.ts deleted file mode 100644 index fca628d8405d..000000000000 --- a/packages/node/src/integrations/local-variables/inspector.d.ts +++ /dev/null @@ -1,3387 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable @typescript-eslint/unified-signatures */ -/* eslint-disable @typescript-eslint/explicit-member-accessibility */ -/* eslint-disable max-lines */ -/* eslint-disable @typescript-eslint/ban-types */ -// Type definitions for inspector - -// These definitions were copied from: -// https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/d37bf642ed2f3fe403e405892e2eb4240a191bb0/types/node/inspector.d.ts - -/** - * The `inspector` module provides an API for interacting with the V8 inspector. - * - * It can be accessed using: - * - * ```js - * const inspector = require('inspector'); - * ``` - * @see [source](https://github.com/nodejs/node/blob/v18.0.0/lib/inspector.js) - */ -declare module 'inspector' { - import EventEmitter = require('node:events'); - interface InspectorNotification { - method: string; - params: T; - } - namespace Schema { - /** - * Description of the protocol domain. - */ - interface Domain { - /** - * Domain name. - */ - name: string; - /** - * Domain version. - */ - version: string; - } - interface GetDomainsReturnType { - /** - * List of supported domains. - */ - domains: Domain[]; - } - } - namespace Runtime { - /** - * Unique script identifier. - */ - type ScriptId = string; - /** - * Unique object identifier. - */ - type RemoteObjectId = string; - /** - * Primitive value which cannot be JSON-stringified. - */ - type UnserializableValue = string; - /** - * Mirror object referencing original JavaScript object. - */ - interface RemoteObject { - /** - * Object type. - */ - type: string; - /** - * Object subtype hint. Specified for object type values only. - */ - subtype?: string | undefined; - /** - * Object class (constructor) name. Specified for object type values only. - */ - className?: string | undefined; - /** - * Remote object value in case of primitive values or JSON values (if it was requested). - */ - value?: any; - /** - * Primitive value which can not be JSON-stringified does not have value, but gets this property. - */ - unserializableValue?: UnserializableValue | undefined; - /** - * String representation of the object. - */ - description?: string | undefined; - /** - * Unique object identifier (for non-primitive values). - */ - objectId?: RemoteObjectId | undefined; - /** - * Preview containing abbreviated property values. Specified for object type values only. - * @experimental - */ - preview?: ObjectPreview | undefined; - /** - * @experimental - */ - customPreview?: CustomPreview | undefined; - } - /** - * @experimental - */ - interface CustomPreview { - header: string; - hasBody: boolean; - formatterObjectId: RemoteObjectId; - bindRemoteObjectFunctionId: RemoteObjectId; - configObjectId?: RemoteObjectId | undefined; - } - /** - * Object containing abbreviated remote object value. - * @experimental - */ - interface ObjectPreview { - /** - * Object type. - */ - type: string; - /** - * Object subtype hint. Specified for object type values only. - */ - subtype?: string | undefined; - /** - * String representation of the object. - */ - description?: string | undefined; - /** - * True iff some of the properties or entries of the original object did not fit. - */ - overflow: boolean; - /** - * List of the properties. - */ - properties: PropertyPreview[]; - /** - * List of the entries. Specified for map and set subtype values only. - */ - entries?: EntryPreview[] | undefined; - } - /** - * @experimental - */ - interface PropertyPreview { - /** - * Property name. - */ - name: string; - /** - * Object type. Accessor means that the property itself is an accessor property. - */ - type: string; - /** - * User-friendly property value string. - */ - value?: string | undefined; - /** - * Nested value preview. - */ - valuePreview?: ObjectPreview | undefined; - /** - * Object subtype hint. Specified for object type values only. - */ - subtype?: string | undefined; - } - /** - * @experimental - */ - interface EntryPreview { - /** - * Preview of the key. Specified for map-like collection entries. - */ - key?: ObjectPreview | undefined; - /** - * Preview of the value. - */ - value: ObjectPreview; - } - /** - * Object property descriptor. - */ - interface PropertyDescriptor { - /** - * Property name or symbol description. - */ - name: string; - /** - * The value associated with the property. - */ - value?: RemoteObject | undefined; - /** - * True if the value associated with the property may be changed (data descriptors only). - */ - writable?: boolean | undefined; - /** - * A function which serves as a getter for the property, or undefined if there is no getter (accessor descriptors only). - */ - get?: RemoteObject | undefined; - /** - * A function which serves as a setter for the property, or undefined if there is no setter (accessor descriptors only). - */ - set?: RemoteObject | undefined; - /** - * True if the type of this property descriptor may be changed and if the property may be deleted from the corresponding object. - */ - configurable: boolean; - /** - * True if this property shows up during enumeration of the properties on the corresponding object. - */ - enumerable: boolean; - /** - * True if the result was thrown during the evaluation. - */ - wasThrown?: boolean | undefined; - /** - * True if the property is owned for the object. - */ - isOwn?: boolean | undefined; - /** - * Property symbol object, if the property is of the symbol type. - */ - symbol?: RemoteObject | undefined; - } - /** - * Object internal property descriptor. This property isn't normally visible in JavaScript code. - */ - interface InternalPropertyDescriptor { - /** - * Conventional property name. - */ - name: string; - /** - * The value associated with the property. - */ - value?: RemoteObject | undefined; - } - /** - * Represents function call argument. Either remote object id objectId, primitive value, unserializable primitive value or neither of (for undefined) them should be specified. - */ - interface CallArgument { - /** - * Primitive value or serializable javascript object. - */ - value?: any; - /** - * Primitive value which can not be JSON-stringified. - */ - unserializableValue?: UnserializableValue | undefined; - /** - * Remote object handle. - */ - objectId?: RemoteObjectId | undefined; - } - /** - * Id of an execution context. - */ - type ExecutionContextId = number; - /** - * Description of an isolated world. - */ - interface ExecutionContextDescription { - /** - * Unique id of the execution context. It can be used to specify in which execution context script evaluation should be performed. - */ - id: ExecutionContextId; - /** - * Execution context origin. - */ - origin: string; - /** - * Human readable name describing given context. - */ - name: string; - /** - * Embedder-specific auxiliary data. - */ - auxData?: {} | undefined; - } - /** - * Detailed information about exception (or error) that was thrown during script compilation or execution. - */ - interface ExceptionDetails { - /** - * Exception id. - */ - exceptionId: number; - /** - * Exception text, which should be used together with exception object when available. - */ - text: string; - /** - * Line number of the exception location (0-based). - */ - lineNumber: number; - /** - * Column number of the exception location (0-based). - */ - columnNumber: number; - /** - * Script ID of the exception location. - */ - scriptId?: ScriptId | undefined; - /** - * URL of the exception location, to be used when the script was not reported. - */ - url?: string | undefined; - /** - * JavaScript stack trace if available. - */ - stackTrace?: StackTrace | undefined; - /** - * Exception object if available. - */ - exception?: RemoteObject | undefined; - /** - * Identifier of the context where exception happened. - */ - executionContextId?: ExecutionContextId | undefined; - } - /** - * Number of milliseconds since epoch. - */ - type Timestamp = number; - /** - * Stack entry for runtime errors and assertions. - */ - interface CallFrame { - /** - * JavaScript function name. - */ - functionName: string; - /** - * JavaScript script id. - */ - scriptId: ScriptId; - /** - * JavaScript script name or url. - */ - url: string; - /** - * JavaScript script line number (0-based). - */ - lineNumber: number; - /** - * JavaScript script column number (0-based). - */ - columnNumber: number; - } - /** - * Call frames for assertions or error messages. - */ - interface StackTrace { - /** - * String label of this stack trace. For async traces this may be a name of the function that initiated the async call. - */ - description?: string | undefined; - /** - * JavaScript function name. - */ - callFrames: CallFrame[]; - /** - * Asynchronous JavaScript stack trace that preceded this stack, if available. - */ - parent?: StackTrace | undefined; - /** - * Asynchronous JavaScript stack trace that preceded this stack, if available. - * @experimental - */ - parentId?: StackTraceId | undefined; - } - /** - * Unique identifier of current debugger. - * @experimental - */ - type UniqueDebuggerId = string; - /** - * If debuggerId is set stack trace comes from another debugger and can be resolved there. This allows to track cross-debugger calls. See Runtime.StackTrace and Debugger.paused for usages. - * @experimental - */ - interface StackTraceId { - id: string; - debuggerId?: UniqueDebuggerId | undefined; - } - interface EvaluateParameterType { - /** - * Expression to evaluate. - */ - expression: string; - /** - * Symbolic group name that can be used to release multiple objects. - */ - objectGroup?: string | undefined; - /** - * Determines whether Command Line API should be available during the evaluation. - */ - includeCommandLineAPI?: boolean | undefined; - /** - * In silent mode exceptions thrown during evaluation are not reported and do not pause execution. Overrides setPauseOnException state. - */ - silent?: boolean | undefined; - /** - * Specifies in which execution context to perform evaluation. If the parameter is omitted the evaluation will be performed in the context of the inspected page. - */ - contextId?: ExecutionContextId | undefined; - /** - * Whether the result is expected to be a JSON object that should be sent by value. - */ - returnByValue?: boolean | undefined; - /** - * Whether preview should be generated for the result. - * @experimental - */ - generatePreview?: boolean | undefined; - /** - * Whether execution should be treated as initiated by user in the UI. - */ - userGesture?: boolean | undefined; - /** - * Whether execution should await for resulting value and return once awaited promise is resolved. - */ - awaitPromise?: boolean | undefined; - } - interface AwaitPromiseParameterType { - /** - * Identifier of the promise. - */ - promiseObjectId: RemoteObjectId; - /** - * Whether the result is expected to be a JSON object that should be sent by value. - */ - returnByValue?: boolean | undefined; - /** - * Whether preview should be generated for the result. - */ - generatePreview?: boolean | undefined; - } - interface CallFunctionOnParameterType { - /** - * Declaration of the function to call. - */ - functionDeclaration: string; - /** - * Identifier of the object to call function on. Either objectId or executionContextId should be specified. - */ - objectId?: RemoteObjectId | undefined; - /** - * Call arguments. All call arguments must belong to the same JavaScript world as the target object. - */ - arguments?: CallArgument[] | undefined; - /** - * In silent mode exceptions thrown during evaluation are not reported and do not pause execution. Overrides setPauseOnException state. - */ - silent?: boolean | undefined; - /** - * Whether the result is expected to be a JSON object which should be sent by value. - */ - returnByValue?: boolean | undefined; - /** - * Whether preview should be generated for the result. - * @experimental - */ - generatePreview?: boolean | undefined; - /** - * Whether execution should be treated as initiated by user in the UI. - */ - userGesture?: boolean | undefined; - /** - * Whether execution should await for resulting value and return once awaited promise is resolved. - */ - awaitPromise?: boolean | undefined; - /** - * Specifies execution context which global object will be used to call function on. Either executionContextId or objectId should be specified. - */ - executionContextId?: ExecutionContextId | undefined; - /** - * Symbolic group name that can be used to release multiple objects. If objectGroup is not specified and objectId is, objectGroup will be inherited from object. - */ - objectGroup?: string | undefined; - } - interface GetPropertiesParameterType { - /** - * Identifier of the object to return properties for. - */ - objectId: RemoteObjectId; - /** - * If true, returns properties belonging only to the element itself, not to its prototype chain. - */ - ownProperties?: boolean | undefined; - /** - * If true, returns accessor properties (with getter/setter) only; internal properties are not returned either. - * @experimental - */ - accessorPropertiesOnly?: boolean | undefined; - /** - * Whether preview should be generated for the results. - * @experimental - */ - generatePreview?: boolean | undefined; - } - interface ReleaseObjectParameterType { - /** - * Identifier of the object to release. - */ - objectId: RemoteObjectId; - } - interface ReleaseObjectGroupParameterType { - /** - * Symbolic object group name. - */ - objectGroup: string; - } - interface SetCustomObjectFormatterEnabledParameterType { - enabled: boolean; - } - interface CompileScriptParameterType { - /** - * Expression to compile. - */ - expression: string; - /** - * Source url to be set for the script. - */ - sourceURL: string; - /** - * Specifies whether the compiled script should be persisted. - */ - persistScript: boolean; - /** - * Specifies in which execution context to perform script run. If the parameter is omitted the evaluation will be performed in the context of the inspected page. - */ - executionContextId?: ExecutionContextId | undefined; - } - interface RunScriptParameterType { - /** - * Id of the script to run. - */ - scriptId: ScriptId; - /** - * Specifies in which execution context to perform script run. If the parameter is omitted the evaluation will be performed in the context of the inspected page. - */ - executionContextId?: ExecutionContextId | undefined; - /** - * Symbolic group name that can be used to release multiple objects. - */ - objectGroup?: string | undefined; - /** - * In silent mode exceptions thrown during evaluation are not reported and do not pause execution. Overrides setPauseOnException state. - */ - silent?: boolean | undefined; - /** - * Determines whether Command Line API should be available during the evaluation. - */ - includeCommandLineAPI?: boolean | undefined; - /** - * Whether the result is expected to be a JSON object which should be sent by value. - */ - returnByValue?: boolean | undefined; - /** - * Whether preview should be generated for the result. - */ - generatePreview?: boolean | undefined; - /** - * Whether execution should await for resulting value and return once awaited promise is resolved. - */ - awaitPromise?: boolean | undefined; - } - interface QueryObjectsParameterType { - /** - * Identifier of the prototype to return objects for. - */ - prototypeObjectId: RemoteObjectId; - } - interface GlobalLexicalScopeNamesParameterType { - /** - * Specifies in which execution context to lookup global scope variables. - */ - executionContextId?: ExecutionContextId | undefined; - } - interface EvaluateReturnType { - /** - * Evaluation result. - */ - result: RemoteObject; - /** - * Exception details. - */ - exceptionDetails?: ExceptionDetails | undefined; - } - interface AwaitPromiseReturnType { - /** - * Promise result. Will contain rejected value if promise was rejected. - */ - result: RemoteObject; - /** - * Exception details if stack strace is available. - */ - exceptionDetails?: ExceptionDetails | undefined; - } - interface CallFunctionOnReturnType { - /** - * Call result. - */ - result: RemoteObject; - /** - * Exception details. - */ - exceptionDetails?: ExceptionDetails | undefined; - } - interface GetPropertiesReturnType { - /** - * Object properties. - */ - result: PropertyDescriptor[]; - /** - * Internal object properties (only of the element itself). - */ - internalProperties?: InternalPropertyDescriptor[] | undefined; - /** - * Exception details. - */ - exceptionDetails?: ExceptionDetails | undefined; - } - interface CompileScriptReturnType { - /** - * Id of the script. - */ - scriptId?: ScriptId | undefined; - /** - * Exception details. - */ - exceptionDetails?: ExceptionDetails | undefined; - } - interface RunScriptReturnType { - /** - * Run result. - */ - result: RemoteObject; - /** - * Exception details. - */ - exceptionDetails?: ExceptionDetails | undefined; - } - interface QueryObjectsReturnType { - /** - * Array with objects. - */ - objects: RemoteObject; - } - interface GlobalLexicalScopeNamesReturnType { - names: string[]; - } - interface ExecutionContextCreatedEventDataType { - /** - * A newly created execution context. - */ - context: ExecutionContextDescription; - } - interface ExecutionContextDestroyedEventDataType { - /** - * Id of the destroyed context - */ - executionContextId: ExecutionContextId; - } - interface ExceptionThrownEventDataType { - /** - * Timestamp of the exception. - */ - timestamp: Timestamp; - exceptionDetails: ExceptionDetails; - } - interface ExceptionRevokedEventDataType { - /** - * Reason describing why exception was revoked. - */ - reason: string; - /** - * The id of revoked exception, as reported in exceptionThrown. - */ - exceptionId: number; - } - interface ConsoleAPICalledEventDataType { - /** - * Type of the call. - */ - type: string; - /** - * Call arguments. - */ - args: RemoteObject[]; - /** - * Identifier of the context where the call was made. - */ - executionContextId: ExecutionContextId; - /** - * Call timestamp. - */ - timestamp: Timestamp; - /** - * Stack trace captured when the call was made. - */ - stackTrace?: StackTrace | undefined; - /** - * Console context descriptor for calls on non-default console context (not console.*): 'anonymous#unique-logger-id' for call on unnamed context, 'name#unique-logger-id' for call on named context. - * @experimental - */ - context?: string | undefined; - } - interface InspectRequestedEventDataType { - object: RemoteObject; - hints: {}; - } - } - namespace Debugger { - /** - * Breakpoint identifier. - */ - type BreakpointId = string; - /** - * Call frame identifier. - */ - type CallFrameId = string; - /** - * Location in the source code. - */ - interface Location { - /** - * Script identifier as reported in the Debugger.scriptParsed. - */ - scriptId: Runtime.ScriptId; - /** - * Line number in the script (0-based). - */ - lineNumber: number; - /** - * Column number in the script (0-based). - */ - columnNumber?: number | undefined; - } - /** - * Location in the source code. - * @experimental - */ - interface ScriptPosition { - lineNumber: number; - columnNumber: number; - } - /** - * JavaScript call frame. Array of call frames form the call stack. - */ - interface CallFrame { - /** - * Call frame identifier. This identifier is only valid while the virtual machine is paused. - */ - callFrameId: CallFrameId; - /** - * Name of the JavaScript function called on this call frame. - */ - functionName: string; - /** - * Location in the source code. - */ - functionLocation?: Location | undefined; - /** - * Location in the source code. - */ - location: Location; - /** - * JavaScript script name or url. - */ - url: string; - /** - * Scope chain for this call frame. - */ - scopeChain: Scope[]; - /** - * this object for this call frame. - */ - this: Runtime.RemoteObject; - /** - * The value being returned, if the function is at return point. - */ - returnValue?: Runtime.RemoteObject | undefined; - } - /** - * Scope description. - */ - interface Scope { - /** - * Scope type. - */ - type: string; - /** - * Object representing the scope. For global and with scopes it represents the actual object; for the rest of the scopes, it is artificial transient object enumerating scope variables as its properties. - */ - object: Runtime.RemoteObject; - name?: string | undefined; - /** - * Location in the source code where scope starts - */ - startLocation?: Location | undefined; - /** - * Location in the source code where scope ends - */ - endLocation?: Location | undefined; - } - /** - * Search match for resource. - */ - interface SearchMatch { - /** - * Line number in resource content. - */ - lineNumber: number; - /** - * Line with match content. - */ - lineContent: string; - } - interface BreakLocation { - /** - * Script identifier as reported in the Debugger.scriptParsed. - */ - scriptId: Runtime.ScriptId; - /** - * Line number in the script (0-based). - */ - lineNumber: number; - /** - * Column number in the script (0-based). - */ - columnNumber?: number | undefined; - type?: string | undefined; - } - interface SetBreakpointsActiveParameterType { - /** - * New value for breakpoints active state. - */ - active: boolean; - } - interface SetSkipAllPausesParameterType { - /** - * New value for skip pauses state. - */ - skip: boolean; - } - interface SetBreakpointByUrlParameterType { - /** - * Line number to set breakpoint at. - */ - lineNumber: number; - /** - * URL of the resources to set breakpoint on. - */ - url?: string | undefined; - /** - * Regex pattern for the URLs of the resources to set breakpoints on. Either url or urlRegex must be specified. - */ - urlRegex?: string | undefined; - /** - * Script hash of the resources to set breakpoint on. - */ - scriptHash?: string | undefined; - /** - * Offset in the line to set breakpoint at. - */ - columnNumber?: number | undefined; - /** - * Expression to use as a breakpoint condition. When specified, debugger will only stop on the breakpoint if this expression evaluates to true. - */ - condition?: string | undefined; - } - interface SetBreakpointParameterType { - /** - * Location to set breakpoint in. - */ - location: Location; - /** - * Expression to use as a breakpoint condition. When specified, debugger will only stop on the breakpoint if this expression evaluates to true. - */ - condition?: string | undefined; - } - interface RemoveBreakpointParameterType { - breakpointId: BreakpointId; - } - interface GetPossibleBreakpointsParameterType { - /** - * Start of range to search possible breakpoint locations in. - */ - start: Location; - /** - * End of range to search possible breakpoint locations in (excluding). When not specified, end of scripts is used as end of range. - */ - end?: Location | undefined; - /** - * Only consider locations which are in the same (non-nested) function as start. - */ - restrictToFunction?: boolean | undefined; - } - interface ContinueToLocationParameterType { - /** - * Location to continue to. - */ - location: Location; - targetCallFrames?: string | undefined; - } - interface PauseOnAsyncCallParameterType { - /** - * Debugger will pause when async call with given stack trace is started. - */ - parentStackTraceId: Runtime.StackTraceId; - } - interface StepIntoParameterType { - /** - * Debugger will issue additional Debugger.paused notification if any async task is scheduled before next pause. - * @experimental - */ - breakOnAsyncCall?: boolean | undefined; - } - interface GetStackTraceParameterType { - stackTraceId: Runtime.StackTraceId; - } - interface SearchInContentParameterType { - /** - * Id of the script to search in. - */ - scriptId: Runtime.ScriptId; - /** - * String to search for. - */ - query: string; - /** - * If true, search is case sensitive. - */ - caseSensitive?: boolean | undefined; - /** - * If true, treats string parameter as regex. - */ - isRegex?: boolean | undefined; - } - interface SetScriptSourceParameterType { - /** - * Id of the script to edit. - */ - scriptId: Runtime.ScriptId; - /** - * New content of the script. - */ - scriptSource: string; - /** - * If true the change will not actually be applied. Dry run may be used to get result description without actually modifying the code. - */ - dryRun?: boolean | undefined; - } - interface RestartFrameParameterType { - /** - * Call frame identifier to evaluate on. - */ - callFrameId: CallFrameId; - } - interface GetScriptSourceParameterType { - /** - * Id of the script to get source for. - */ - scriptId: Runtime.ScriptId; - } - interface SetPauseOnExceptionsParameterType { - /** - * Pause on exceptions mode. - */ - state: string; - } - interface EvaluateOnCallFrameParameterType { - /** - * Call frame identifier to evaluate on. - */ - callFrameId: CallFrameId; - /** - * Expression to evaluate. - */ - expression: string; - /** - * String object group name to put result into (allows rapid releasing resulting object handles using releaseObjectGroup). - */ - objectGroup?: string | undefined; - /** - * Specifies whether command line API should be available to the evaluated expression, defaults to false. - */ - includeCommandLineAPI?: boolean | undefined; - /** - * In silent mode exceptions thrown during evaluation are not reported and do not pause execution. Overrides setPauseOnException state. - */ - silent?: boolean | undefined; - /** - * Whether the result is expected to be a JSON object that should be sent by value. - */ - returnByValue?: boolean | undefined; - /** - * Whether preview should be generated for the result. - * @experimental - */ - generatePreview?: boolean | undefined; - /** - * Whether to throw an exception if side effect cannot be ruled out during evaluation. - */ - throwOnSideEffect?: boolean | undefined; - } - interface SetVariableValueParameterType { - /** - * 0-based number of scope as was listed in scope chain. Only 'local', 'closure' and 'catch' scope types are allowed. Other scopes could be manipulated manually. - */ - scopeNumber: number; - /** - * Variable name. - */ - variableName: string; - /** - * New variable value. - */ - newValue: Runtime.CallArgument; - /** - * Id of callframe that holds variable. - */ - callFrameId: CallFrameId; - } - interface SetReturnValueParameterType { - /** - * New return value. - */ - newValue: Runtime.CallArgument; - } - interface SetAsyncCallStackDepthParameterType { - /** - * Maximum depth of async call stacks. Setting to 0 will effectively disable collecting async call stacks (default). - */ - maxDepth: number; - } - interface SetBlackboxPatternsParameterType { - /** - * Array of regexps that will be used to check script url for blackbox state. - */ - patterns: string[]; - } - interface SetBlackboxedRangesParameterType { - /** - * Id of the script. - */ - scriptId: Runtime.ScriptId; - positions: ScriptPosition[]; - } - interface EnableReturnType { - /** - * Unique identifier of the debugger. - * @experimental - */ - debuggerId: Runtime.UniqueDebuggerId; - } - interface SetBreakpointByUrlReturnType { - /** - * Id of the created breakpoint for further reference. - */ - breakpointId: BreakpointId; - /** - * List of the locations this breakpoint resolved into upon addition. - */ - locations: Location[]; - } - interface SetBreakpointReturnType { - /** - * Id of the created breakpoint for further reference. - */ - breakpointId: BreakpointId; - /** - * Location this breakpoint resolved into. - */ - actualLocation: Location; - } - interface GetPossibleBreakpointsReturnType { - /** - * List of the possible breakpoint locations. - */ - locations: BreakLocation[]; - } - interface GetStackTraceReturnType { - stackTrace: Runtime.StackTrace; - } - interface SearchInContentReturnType { - /** - * List of search matches. - */ - result: SearchMatch[]; - } - interface SetScriptSourceReturnType { - /** - * New stack trace in case editing has happened while VM was stopped. - */ - callFrames?: CallFrame[] | undefined; - /** - * Whether current call stack was modified after applying the changes. - */ - stackChanged?: boolean | undefined; - /** - * Async stack trace, if any. - */ - asyncStackTrace?: Runtime.StackTrace | undefined; - /** - * Async stack trace, if any. - * @experimental - */ - asyncStackTraceId?: Runtime.StackTraceId | undefined; - /** - * Exception details if any. - */ - exceptionDetails?: Runtime.ExceptionDetails | undefined; - } - interface RestartFrameReturnType { - /** - * New stack trace. - */ - callFrames: CallFrame[]; - /** - * Async stack trace, if any. - */ - asyncStackTrace?: Runtime.StackTrace | undefined; - /** - * Async stack trace, if any. - * @experimental - */ - asyncStackTraceId?: Runtime.StackTraceId | undefined; - } - interface GetScriptSourceReturnType { - /** - * Script source. - */ - scriptSource: string; - } - interface EvaluateOnCallFrameReturnType { - /** - * Object wrapper for the evaluation result. - */ - result: Runtime.RemoteObject; - /** - * Exception details. - */ - exceptionDetails?: Runtime.ExceptionDetails | undefined; - } - interface ScriptParsedEventDataType { - /** - * Identifier of the script parsed. - */ - scriptId: Runtime.ScriptId; - /** - * URL or name of the script parsed (if any). - */ - url: string; - /** - * Line offset of the script within the resource with given URL (for script tags). - */ - startLine: number; - /** - * Column offset of the script within the resource with given URL. - */ - startColumn: number; - /** - * Last line of the script. - */ - endLine: number; - /** - * Length of the last line of the script. - */ - endColumn: number; - /** - * Specifies script creation context. - */ - executionContextId: Runtime.ExecutionContextId; - /** - * Content hash of the script. - */ - hash: string; - /** - * Embedder-specific auxiliary data. - */ - executionContextAuxData?: {} | undefined; - /** - * True, if this script is generated as a result of the live edit operation. - * @experimental - */ - isLiveEdit?: boolean | undefined; - /** - * URL of source map associated with script (if any). - */ - sourceMapURL?: string | undefined; - /** - * True, if this script has sourceURL. - */ - hasSourceURL?: boolean | undefined; - /** - * True, if this script is ES6 module. - */ - isModule?: boolean | undefined; - /** - * This script length. - */ - length?: number | undefined; - /** - * JavaScript top stack frame of where the script parsed event was triggered if available. - * @experimental - */ - stackTrace?: Runtime.StackTrace | undefined; - } - interface ScriptFailedToParseEventDataType { - /** - * Identifier of the script parsed. - */ - scriptId: Runtime.ScriptId; - /** - * URL or name of the script parsed (if any). - */ - url: string; - /** - * Line offset of the script within the resource with given URL (for script tags). - */ - startLine: number; - /** - * Column offset of the script within the resource with given URL. - */ - startColumn: number; - /** - * Last line of the script. - */ - endLine: number; - /** - * Length of the last line of the script. - */ - endColumn: number; - /** - * Specifies script creation context. - */ - executionContextId: Runtime.ExecutionContextId; - /** - * Content hash of the script. - */ - hash: string; - /** - * Embedder-specific auxiliary data. - */ - executionContextAuxData?: {} | undefined; - /** - * URL of source map associated with script (if any). - */ - sourceMapURL?: string | undefined; - /** - * True, if this script has sourceURL. - */ - hasSourceURL?: boolean | undefined; - /** - * True, if this script is ES6 module. - */ - isModule?: boolean | undefined; - /** - * This script length. - */ - length?: number | undefined; - /** - * JavaScript top stack frame of where the script parsed event was triggered if available. - * @experimental - */ - stackTrace?: Runtime.StackTrace | undefined; - } - interface BreakpointResolvedEventDataType { - /** - * Breakpoint unique identifier. - */ - breakpointId: BreakpointId; - /** - * Actual breakpoint location. - */ - location: Location; - } - interface PausedEventDataType { - /** - * Call stack the virtual machine stopped on. - */ - callFrames: CallFrame[]; - /** - * Pause reason. - */ - reason: string; - /** - * Object containing break-specific auxiliary properties. - */ - data?: {} | undefined; - /** - * Hit breakpoints IDs - */ - hitBreakpoints?: string[] | undefined; - /** - * Async stack trace, if any. - */ - asyncStackTrace?: Runtime.StackTrace | undefined; - /** - * Async stack trace, if any. - * @experimental - */ - asyncStackTraceId?: Runtime.StackTraceId | undefined; - /** - * Just scheduled async call will have this stack trace as parent stack during async execution. This field is available only after Debugger.stepInto call with breakOnAsynCall flag. - * @experimental - */ - asyncCallStackTraceId?: Runtime.StackTraceId | undefined; - } - } - namespace Console { - /** - * Console message. - */ - interface ConsoleMessage { - /** - * Message source. - */ - source: string; - /** - * Message severity. - */ - level: string; - /** - * Message text. - */ - text: string; - /** - * URL of the message origin. - */ - url?: string | undefined; - /** - * Line number in the resource that generated this message (1-based). - */ - line?: number | undefined; - /** - * Column number in the resource that generated this message (1-based). - */ - column?: number | undefined; - } - interface MessageAddedEventDataType { - /** - * Console message that has been added. - */ - message: ConsoleMessage; - } - } - namespace Profiler { - /** - * Profile node. Holds callsite information, execution statistics and child nodes. - */ - interface ProfileNode { - /** - * Unique id of the node. - */ - id: number; - /** - * Function location. - */ - callFrame: Runtime.CallFrame; - /** - * Number of samples where this node was on top of the call stack. - */ - hitCount?: number | undefined; - /** - * Child node ids. - */ - children?: number[] | undefined; - /** - * The reason of being not optimized. The function may be deoptimized or marked as don't optimize. - */ - deoptReason?: string | undefined; - /** - * An array of source position ticks. - */ - positionTicks?: PositionTickInfo[] | undefined; - } - /** - * Profile. - */ - interface Profile { - /** - * The list of profile nodes. First item is the root node. - */ - nodes: ProfileNode[]; - /** - * Profiling start timestamp in microseconds. - */ - startTime: number; - /** - * Profiling end timestamp in microseconds. - */ - endTime: number; - /** - * Ids of samples top nodes. - */ - samples?: number[] | undefined; - /** - * Time intervals between adjacent samples in microseconds. The first delta is relative to the profile startTime. - */ - timeDeltas?: number[] | undefined; - } - /** - * Specifies a number of samples attributed to a certain source position. - */ - interface PositionTickInfo { - /** - * Source line number (1-based). - */ - line: number; - /** - * Number of samples attributed to the source line. - */ - ticks: number; - } - /** - * Coverage data for a source range. - */ - interface CoverageRange { - /** - * JavaScript script source offset for the range start. - */ - startOffset: number; - /** - * JavaScript script source offset for the range end. - */ - endOffset: number; - /** - * Collected execution count of the source range. - */ - count: number; - } - /** - * Coverage data for a JavaScript function. - */ - interface FunctionCoverage { - /** - * JavaScript function name. - */ - functionName: string; - /** - * Source ranges inside the function with coverage data. - */ - ranges: CoverageRange[]; - /** - * Whether coverage data for this function has block granularity. - */ - isBlockCoverage: boolean; - } - /** - * Coverage data for a JavaScript script. - */ - interface ScriptCoverage { - /** - * JavaScript script id. - */ - scriptId: Runtime.ScriptId; - /** - * JavaScript script name or url. - */ - url: string; - /** - * Functions contained in the script that has coverage data. - */ - functions: FunctionCoverage[]; - } - /** - * Describes a type collected during runtime. - * @experimental - */ - interface TypeObject { - /** - * Name of a type collected with type profiling. - */ - name: string; - } - /** - * Source offset and types for a parameter or return value. - * @experimental - */ - interface TypeProfileEntry { - /** - * Source offset of the parameter or end of function for return values. - */ - offset: number; - /** - * The types for this parameter or return value. - */ - types: TypeObject[]; - } - /** - * Type profile data collected during runtime for a JavaScript script. - * @experimental - */ - interface ScriptTypeProfile { - /** - * JavaScript script id. - */ - scriptId: Runtime.ScriptId; - /** - * JavaScript script name or url. - */ - url: string; - /** - * Type profile entries for parameters and return values of the functions in the script. - */ - entries: TypeProfileEntry[]; - } - interface SetSamplingIntervalParameterType { - /** - * New sampling interval in microseconds. - */ - interval: number; - } - interface StartPreciseCoverageParameterType { - /** - * Collect accurate call counts beyond simple 'covered' or 'not covered'. - */ - callCount?: boolean | undefined; - /** - * Collect block-based coverage. - */ - detailed?: boolean | undefined; - } - interface StopReturnType { - /** - * Recorded profile. - */ - profile: Profile; - } - interface TakePreciseCoverageReturnType { - /** - * Coverage data for the current isolate. - */ - result: ScriptCoverage[]; - } - interface GetBestEffortCoverageReturnType { - /** - * Coverage data for the current isolate. - */ - result: ScriptCoverage[]; - } - interface TakeTypeProfileReturnType { - /** - * Type profile for all scripts since startTypeProfile() was turned on. - */ - result: ScriptTypeProfile[]; - } - interface ConsoleProfileStartedEventDataType { - id: string; - /** - * Location of console.profile(). - */ - location: Debugger.Location; - /** - * Profile title passed as an argument to console.profile(). - */ - title?: string | undefined; - } - interface ConsoleProfileFinishedEventDataType { - id: string; - /** - * Location of console.profileEnd(). - */ - location: Debugger.Location; - profile: Profile; - /** - * Profile title passed as an argument to console.profile(). - */ - title?: string | undefined; - } - } - namespace HeapProfiler { - /** - * Heap snapshot object id. - */ - type HeapSnapshotObjectId = string; - /** - * Sampling Heap Profile node. Holds callsite information, allocation statistics and child nodes. - */ - interface SamplingHeapProfileNode { - /** - * Function location. - */ - callFrame: Runtime.CallFrame; - /** - * Allocations size in bytes for the node excluding children. - */ - selfSize: number; - /** - * Child nodes. - */ - children: SamplingHeapProfileNode[]; - } - /** - * Profile. - */ - interface SamplingHeapProfile { - head: SamplingHeapProfileNode; - } - interface StartTrackingHeapObjectsParameterType { - trackAllocations?: boolean | undefined; - } - interface StopTrackingHeapObjectsParameterType { - /** - * If true 'reportHeapSnapshotProgress' events will be generated while snapshot is being taken when the tracking is stopped. - */ - reportProgress?: boolean | undefined; - } - interface TakeHeapSnapshotParameterType { - /** - * If true 'reportHeapSnapshotProgress' events will be generated while snapshot is being taken. - */ - reportProgress?: boolean | undefined; - } - interface GetObjectByHeapObjectIdParameterType { - objectId: HeapSnapshotObjectId; - /** - * Symbolic group name that can be used to release multiple objects. - */ - objectGroup?: string | undefined; - } - interface AddInspectedHeapObjectParameterType { - /** - * Heap snapshot object id to be accessible by means of $x command line API. - */ - heapObjectId: HeapSnapshotObjectId; - } - interface GetHeapObjectIdParameterType { - /** - * Identifier of the object to get heap object id for. - */ - objectId: Runtime.RemoteObjectId; - } - interface StartSamplingParameterType { - /** - * Average sample interval in bytes. Poisson distribution is used for the intervals. The default value is 32768 bytes. - */ - samplingInterval?: number | undefined; - } - interface GetObjectByHeapObjectIdReturnType { - /** - * Evaluation result. - */ - result: Runtime.RemoteObject; - } - interface GetHeapObjectIdReturnType { - /** - * Id of the heap snapshot object corresponding to the passed remote object id. - */ - heapSnapshotObjectId: HeapSnapshotObjectId; - } - interface StopSamplingReturnType { - /** - * Recorded sampling heap profile. - */ - profile: SamplingHeapProfile; - } - interface GetSamplingProfileReturnType { - /** - * Return the sampling profile being collected. - */ - profile: SamplingHeapProfile; - } - interface AddHeapSnapshotChunkEventDataType { - chunk: string; - } - interface ReportHeapSnapshotProgressEventDataType { - done: number; - total: number; - finished?: boolean | undefined; - } - interface LastSeenObjectIdEventDataType { - lastSeenObjectId: number; - timestamp: number; - } - interface HeapStatsUpdateEventDataType { - /** - * An array of triplets. Each triplet describes a fragment. The first integer is the fragment index, the second integer is a total count of objects for the fragment, the third integer is a total size of the objects for the fragment. - */ - statsUpdate: number[]; - } - } - namespace NodeTracing { - interface TraceConfig { - /** - * Controls how the trace buffer stores data. - */ - recordMode?: string | undefined; - /** - * Included category filters. - */ - includedCategories: string[]; - } - interface StartParameterType { - traceConfig: TraceConfig; - } - interface GetCategoriesReturnType { - /** - * A list of supported tracing categories. - */ - categories: string[]; - } - interface DataCollectedEventDataType { - value: Array<{}>; - } - } - namespace NodeWorker { - type WorkerID = string; - /** - * Unique identifier of attached debugging session. - */ - type SessionID = string; - interface WorkerInfo { - workerId: WorkerID; - type: string; - title: string; - url: string; - } - interface SendMessageToWorkerParameterType { - message: string; - /** - * Identifier of the session. - */ - sessionId: SessionID; - } - interface EnableParameterType { - /** - * Whether to new workers should be paused until the frontend sends `Runtime.runIfWaitingForDebugger` - * message to run them. - */ - waitForDebuggerOnStart: boolean; - } - interface DetachParameterType { - sessionId: SessionID; - } - interface AttachedToWorkerEventDataType { - /** - * Identifier assigned to the session used to send/receive messages. - */ - sessionId: SessionID; - workerInfo: WorkerInfo; - waitingForDebugger: boolean; - } - interface DetachedFromWorkerEventDataType { - /** - * Detached session identifier. - */ - sessionId: SessionID; - } - interface ReceivedMessageFromWorkerEventDataType { - /** - * Identifier of a session which sends a message. - */ - sessionId: SessionID; - message: string; - } - } - namespace NodeRuntime { - interface NotifyWhenWaitingForDisconnectParameterType { - enabled: boolean; - } - } - /** - * The `inspector.Session` is used for dispatching messages to the V8 inspector - * back-end and receiving message responses and notifications. - */ - class Session extends EventEmitter { - /** - * Create a new instance of the inspector.Session class. - * The inspector session needs to be connected through session.connect() before the messages can be dispatched to the inspector backend. - */ - constructor(); - /** - * Connects a session to the inspector back-end. - * @since v8.0.0 - */ - connect(): void; - /** - * Immediately close the session. All pending message callbacks will be called - * with an error. `session.connect()` will need to be called to be able to send - * messages again. Reconnected session will lose all inspector state, such as - * enabled agents or configured breakpoints. - * @since v8.0.0 - */ - disconnect(): void; - /** - * Posts a message to the inspector back-end. `callback` will be notified when - * a response is received. `callback` is a function that accepts two optional - * arguments: error and message-specific result. - * - * ```js - * session.post('Runtime.evaluate', { expression: '2 + 2' }, - * (error, { result }) => console.log(result)); - * // Output: { type: 'number', value: 4, description: '4' } - * ``` - * - * The latest version of the V8 inspector protocol is published on the [Chrome DevTools Protocol Viewer](https://chromedevtools.github.io/devtools-protocol/v8/). - * - * Node.js inspector supports all the Chrome DevTools Protocol domains declared - * by V8\. Chrome DevTools Protocol domain provides an interface for interacting - * with one of the runtime agents used to inspect the application state and listen - * to the run-time events. - * - * ## Example usage - * - * Apart from the debugger, various V8 Profilers are available through the DevTools - * protocol. - * @since v8.0.0 - */ - post(method: string, params?: {}, callback?: (err: Error | null, params?: {}) => void): void; - post(method: string, callback?: (err: Error | null, params?: {}) => void): void; - /** - * Returns supported domains. - */ - post( - method: 'Schema.getDomains', - callback?: (err: Error | null, params: Schema.GetDomainsReturnType) => void, - ): void; - /** - * Evaluates expression on global object. - */ - post( - method: 'Runtime.evaluate', - params?: Runtime.EvaluateParameterType, - callback?: (err: Error | null, params: Runtime.EvaluateReturnType) => void, - ): void; - post(method: 'Runtime.evaluate', callback?: (err: Error | null, params: Runtime.EvaluateReturnType) => void): void; - /** - * Add handler to promise with given promise object id. - */ - post( - method: 'Runtime.awaitPromise', - params?: Runtime.AwaitPromiseParameterType, - callback?: (err: Error | null, params: Runtime.AwaitPromiseReturnType) => void, - ): void; - post( - method: 'Runtime.awaitPromise', - callback?: (err: Error | null, params: Runtime.AwaitPromiseReturnType) => void, - ): void; - /** - * Calls function with given declaration on the given object. Object group of the result is inherited from the target object. - */ - post( - method: 'Runtime.callFunctionOn', - params?: Runtime.CallFunctionOnParameterType, - callback?: (err: Error | null, params: Runtime.CallFunctionOnReturnType) => void, - ): void; - post( - method: 'Runtime.callFunctionOn', - callback?: (err: Error | null, params: Runtime.CallFunctionOnReturnType) => void, - ): void; - /** - * Returns properties of a given object. Object group of the result is inherited from the target object. - */ - post( - method: 'Runtime.getProperties', - params?: Runtime.GetPropertiesParameterType, - callback?: (err: Error | null, params: Runtime.GetPropertiesReturnType) => void, - ): void; - post( - method: 'Runtime.getProperties', - callback?: (err: Error | null, params: Runtime.GetPropertiesReturnType) => void, - ): void; - /** - * Releases remote object with given id. - */ - post( - method: 'Runtime.releaseObject', - params?: Runtime.ReleaseObjectParameterType, - callback?: (err: Error | null) => void, - ): void; - post(method: 'Runtime.releaseObject', callback?: (err: Error | null) => void): void; - /** - * Releases all remote objects that belong to a given group. - */ - post( - method: 'Runtime.releaseObjectGroup', - params?: Runtime.ReleaseObjectGroupParameterType, - callback?: (err: Error | null) => void, - ): void; - post(method: 'Runtime.releaseObjectGroup', callback?: (err: Error | null) => void): void; - /** - * Tells inspected instance to run if it was waiting for debugger to attach. - */ - post(method: 'Runtime.runIfWaitingForDebugger', callback?: (err: Error | null) => void): void; - /** - * Enables reporting of execution contexts creation by means of executionContextCreated event. When the reporting gets enabled the event will be sent immediately for each existing execution context. - */ - post(method: 'Runtime.enable', callback?: (err: Error | null) => void): void; - /** - * Disables reporting of execution contexts creation. - */ - post(method: 'Runtime.disable', callback?: (err: Error | null) => void): void; - /** - * Discards collected exceptions and console API calls. - */ - post(method: 'Runtime.discardConsoleEntries', callback?: (err: Error | null) => void): void; - /** - * @experimental - */ - post( - method: 'Runtime.setCustomObjectFormatterEnabled', - params?: Runtime.SetCustomObjectFormatterEnabledParameterType, - callback?: (err: Error | null) => void, - ): void; - post(method: 'Runtime.setCustomObjectFormatterEnabled', callback?: (err: Error | null) => void): void; - /** - * Compiles expression. - */ - post( - method: 'Runtime.compileScript', - params?: Runtime.CompileScriptParameterType, - callback?: (err: Error | null, params: Runtime.CompileScriptReturnType) => void, - ): void; - post( - method: 'Runtime.compileScript', - callback?: (err: Error | null, params: Runtime.CompileScriptReturnType) => void, - ): void; - /** - * Runs script with given id in a given context. - */ - post( - method: 'Runtime.runScript', - params?: Runtime.RunScriptParameterType, - callback?: (err: Error | null, params: Runtime.RunScriptReturnType) => void, - ): void; - post( - method: 'Runtime.runScript', - callback?: (err: Error | null, params: Runtime.RunScriptReturnType) => void, - ): void; - post( - method: 'Runtime.queryObjects', - params?: Runtime.QueryObjectsParameterType, - callback?: (err: Error | null, params: Runtime.QueryObjectsReturnType) => void, - ): void; - post( - method: 'Runtime.queryObjects', - callback?: (err: Error | null, params: Runtime.QueryObjectsReturnType) => void, - ): void; - /** - * Returns all let, const and class variables from global scope. - */ - post( - method: 'Runtime.globalLexicalScopeNames', - params?: Runtime.GlobalLexicalScopeNamesParameterType, - callback?: (err: Error | null, params: Runtime.GlobalLexicalScopeNamesReturnType) => void, - ): void; - post( - method: 'Runtime.globalLexicalScopeNames', - callback?: (err: Error | null, params: Runtime.GlobalLexicalScopeNamesReturnType) => void, - ): void; - /** - * Enables debugger for the given page. Clients should not assume that the debugging has been enabled until the result for this command is received. - */ - post(method: 'Debugger.enable', callback?: (err: Error | null, params: Debugger.EnableReturnType) => void): void; - /** - * Disables debugger for given page. - */ - post(method: 'Debugger.disable', callback?: (err: Error | null) => void): void; - /** - * Activates / deactivates all breakpoints on the page. - */ - post( - method: 'Debugger.setBreakpointsActive', - params?: Debugger.SetBreakpointsActiveParameterType, - callback?: (err: Error | null) => void, - ): void; - post(method: 'Debugger.setBreakpointsActive', callback?: (err: Error | null) => void): void; - /** - * Makes page not interrupt on any pauses (breakpoint, exception, dom exception etc). - */ - post( - method: 'Debugger.setSkipAllPauses', - params?: Debugger.SetSkipAllPausesParameterType, - callback?: (err: Error | null) => void, - ): void; - post(method: 'Debugger.setSkipAllPauses', callback?: (err: Error | null) => void): void; - /** - * Sets JavaScript breakpoint at given location specified either by URL or URL regex. Once this command is issued, all existing parsed scripts will have breakpoints resolved and returned in locations property. Further matching script parsing will result in subsequent breakpointResolved events issued. This logical breakpoint will survive page reloads. - */ - post( - method: 'Debugger.setBreakpointByUrl', - params?: Debugger.SetBreakpointByUrlParameterType, - callback?: (err: Error | null, params: Debugger.SetBreakpointByUrlReturnType) => void, - ): void; - post( - method: 'Debugger.setBreakpointByUrl', - callback?: (err: Error | null, params: Debugger.SetBreakpointByUrlReturnType) => void, - ): void; - /** - * Sets JavaScript breakpoint at a given location. - */ - post( - method: 'Debugger.setBreakpoint', - params?: Debugger.SetBreakpointParameterType, - callback?: (err: Error | null, params: Debugger.SetBreakpointReturnType) => void, - ): void; - post( - method: 'Debugger.setBreakpoint', - callback?: (err: Error | null, params: Debugger.SetBreakpointReturnType) => void, - ): void; - /** - * Removes JavaScript breakpoint. - */ - post( - method: 'Debugger.removeBreakpoint', - params?: Debugger.RemoveBreakpointParameterType, - callback?: (err: Error | null) => void, - ): void; - post(method: 'Debugger.removeBreakpoint', callback?: (err: Error | null) => void): void; - /** - * Returns possible locations for breakpoint. scriptId in start and end range locations should be the same. - */ - post( - method: 'Debugger.getPossibleBreakpoints', - params?: Debugger.GetPossibleBreakpointsParameterType, - callback?: (err: Error | null, params: Debugger.GetPossibleBreakpointsReturnType) => void, - ): void; - post( - method: 'Debugger.getPossibleBreakpoints', - callback?: (err: Error | null, params: Debugger.GetPossibleBreakpointsReturnType) => void, - ): void; - /** - * Continues execution until specific location is reached. - */ - post( - method: 'Debugger.continueToLocation', - params?: Debugger.ContinueToLocationParameterType, - callback?: (err: Error | null) => void, - ): void; - post(method: 'Debugger.continueToLocation', callback?: (err: Error | null) => void): void; - /** - * @experimental - */ - post( - method: 'Debugger.pauseOnAsyncCall', - params?: Debugger.PauseOnAsyncCallParameterType, - callback?: (err: Error | null) => void, - ): void; - post(method: 'Debugger.pauseOnAsyncCall', callback?: (err: Error | null) => void): void; - /** - * Steps over the statement. - */ - post(method: 'Debugger.stepOver', callback?: (err: Error | null) => void): void; - /** - * Steps into the function call. - */ - post( - method: 'Debugger.stepInto', - params?: Debugger.StepIntoParameterType, - callback?: (err: Error | null) => void, - ): void; - post(method: 'Debugger.stepInto', callback?: (err: Error | null) => void): void; - /** - * Steps out of the function call. - */ - post(method: 'Debugger.stepOut', callback?: (err: Error | null) => void): void; - /** - * Stops on the next JavaScript statement. - */ - post(method: 'Debugger.pause', callback?: (err: Error | null) => void): void; - /** - * This method is deprecated - use Debugger.stepInto with breakOnAsyncCall and Debugger.pauseOnAsyncTask instead. Steps into next scheduled async task if any is scheduled before next pause. Returns success when async task is actually scheduled, returns error if no task were scheduled or another scheduleStepIntoAsync was called. - * @experimental - */ - post(method: 'Debugger.scheduleStepIntoAsync', callback?: (err: Error | null) => void): void; - /** - * Resumes JavaScript execution. - */ - post(method: 'Debugger.resume', callback?: (err: Error | null) => void): void; - /** - * Returns stack trace with given stackTraceId. - * @experimental - */ - post( - method: 'Debugger.getStackTrace', - params?: Debugger.GetStackTraceParameterType, - callback?: (err: Error | null, params: Debugger.GetStackTraceReturnType) => void, - ): void; - post( - method: 'Debugger.getStackTrace', - callback?: (err: Error | null, params: Debugger.GetStackTraceReturnType) => void, - ): void; - /** - * Searches for given string in script content. - */ - post( - method: 'Debugger.searchInContent', - params?: Debugger.SearchInContentParameterType, - callback?: (err: Error | null, params: Debugger.SearchInContentReturnType) => void, - ): void; - post( - method: 'Debugger.searchInContent', - callback?: (err: Error | null, params: Debugger.SearchInContentReturnType) => void, - ): void; - /** - * Edits JavaScript source live. - */ - post( - method: 'Debugger.setScriptSource', - params?: Debugger.SetScriptSourceParameterType, - callback?: (err: Error | null, params: Debugger.SetScriptSourceReturnType) => void, - ): void; - post( - method: 'Debugger.setScriptSource', - callback?: (err: Error | null, params: Debugger.SetScriptSourceReturnType) => void, - ): void; - /** - * Restarts particular call frame from the beginning. - */ - post( - method: 'Debugger.restartFrame', - params?: Debugger.RestartFrameParameterType, - callback?: (err: Error | null, params: Debugger.RestartFrameReturnType) => void, - ): void; - post( - method: 'Debugger.restartFrame', - callback?: (err: Error | null, params: Debugger.RestartFrameReturnType) => void, - ): void; - /** - * Returns source for the script with given id. - */ - post( - method: 'Debugger.getScriptSource', - params?: Debugger.GetScriptSourceParameterType, - callback?: (err: Error | null, params: Debugger.GetScriptSourceReturnType) => void, - ): void; - post( - method: 'Debugger.getScriptSource', - callback?: (err: Error | null, params: Debugger.GetScriptSourceReturnType) => void, - ): void; - /** - * Defines pause on exceptions state. Can be set to stop on all exceptions, uncaught exceptions or no exceptions. Initial pause on exceptions state is none. - */ - post( - method: 'Debugger.setPauseOnExceptions', - params?: Debugger.SetPauseOnExceptionsParameterType, - callback?: (err: Error | null) => void, - ): void; - post(method: 'Debugger.setPauseOnExceptions', callback?: (err: Error | null) => void): void; - /** - * Evaluates expression on a given call frame. - */ - post( - method: 'Debugger.evaluateOnCallFrame', - params?: Debugger.EvaluateOnCallFrameParameterType, - callback?: (err: Error | null, params: Debugger.EvaluateOnCallFrameReturnType) => void, - ): void; - post( - method: 'Debugger.evaluateOnCallFrame', - callback?: (err: Error | null, params: Debugger.EvaluateOnCallFrameReturnType) => void, - ): void; - /** - * Changes value of variable in a callframe. Object-based scopes are not supported and must be mutated manually. - */ - post( - method: 'Debugger.setVariableValue', - params?: Debugger.SetVariableValueParameterType, - callback?: (err: Error | null) => void, - ): void; - post(method: 'Debugger.setVariableValue', callback?: (err: Error | null) => void): void; - /** - * Changes return value in top frame. Available only at return break position. - * @experimental - */ - post( - method: 'Debugger.setReturnValue', - params?: Debugger.SetReturnValueParameterType, - callback?: (err: Error | null) => void, - ): void; - post(method: 'Debugger.setReturnValue', callback?: (err: Error | null) => void): void; - /** - * Enables or disables async call stacks tracking. - */ - post( - method: 'Debugger.setAsyncCallStackDepth', - params?: Debugger.SetAsyncCallStackDepthParameterType, - callback?: (err: Error | null) => void, - ): void; - post(method: 'Debugger.setAsyncCallStackDepth', callback?: (err: Error | null) => void): void; - /** - * Replace previous blackbox patterns with passed ones. Forces backend to skip stepping/pausing in scripts with url matching one of the patterns. VM will try to leave blackboxed script by performing 'step in' several times, finally resorting to 'step out' if unsuccessful. - * @experimental - */ - post( - method: 'Debugger.setBlackboxPatterns', - params?: Debugger.SetBlackboxPatternsParameterType, - callback?: (err: Error | null) => void, - ): void; - post(method: 'Debugger.setBlackboxPatterns', callback?: (err: Error | null) => void): void; - /** - * Makes backend skip steps in the script in blackboxed ranges. VM will try leave blacklisted scripts by performing 'step in' several times, finally resorting to 'step out' if unsuccessful. Positions array contains positions where blackbox state is changed. First interval isn't blackboxed. Array should be sorted. - * @experimental - */ - post( - method: 'Debugger.setBlackboxedRanges', - params?: Debugger.SetBlackboxedRangesParameterType, - callback?: (err: Error | null) => void, - ): void; - post(method: 'Debugger.setBlackboxedRanges', callback?: (err: Error | null) => void): void; - /** - * Enables console domain, sends the messages collected so far to the client by means of the messageAdded notification. - */ - post(method: 'Console.enable', callback?: (err: Error | null) => void): void; - /** - * Disables console domain, prevents further console messages from being reported to the client. - */ - post(method: 'Console.disable', callback?: (err: Error | null) => void): void; - /** - * Does nothing. - */ - post(method: 'Console.clearMessages', callback?: (err: Error | null) => void): void; - post(method: 'Profiler.enable', callback?: (err: Error | null) => void): void; - post(method: 'Profiler.disable', callback?: (err: Error | null) => void): void; - /** - * Changes CPU profiler sampling interval. Must be called before CPU profiles recording started. - */ - post( - method: 'Profiler.setSamplingInterval', - params?: Profiler.SetSamplingIntervalParameterType, - callback?: (err: Error | null) => void, - ): void; - post(method: 'Profiler.setSamplingInterval', callback?: (err: Error | null) => void): void; - post(method: 'Profiler.start', callback?: (err: Error | null) => void): void; - post(method: 'Profiler.stop', callback?: (err: Error | null, params: Profiler.StopReturnType) => void): void; - /** - * Enable precise code coverage. Coverage data for JavaScript executed before enabling precise code coverage may be incomplete. Enabling prevents running optimized code and resets execution counters. - */ - post( - method: 'Profiler.startPreciseCoverage', - params?: Profiler.StartPreciseCoverageParameterType, - callback?: (err: Error | null) => void, - ): void; - post(method: 'Profiler.startPreciseCoverage', callback?: (err: Error | null) => void): void; - /** - * Disable precise code coverage. Disabling releases unnecessary execution count records and allows executing optimized code. - */ - post(method: 'Profiler.stopPreciseCoverage', callback?: (err: Error | null) => void): void; - /** - * Collect coverage data for the current isolate, and resets execution counters. Precise code coverage needs to have started. - */ - post( - method: 'Profiler.takePreciseCoverage', - callback?: (err: Error | null, params: Profiler.TakePreciseCoverageReturnType) => void, - ): void; - /** - * Collect coverage data for the current isolate. The coverage data may be incomplete due to garbage collection. - */ - post( - method: 'Profiler.getBestEffortCoverage', - callback?: (err: Error | null, params: Profiler.GetBestEffortCoverageReturnType) => void, - ): void; - /** - * Enable type profile. - * @experimental - */ - post(method: 'Profiler.startTypeProfile', callback?: (err: Error | null) => void): void; - /** - * Disable type profile. Disabling releases type profile data collected so far. - * @experimental - */ - post(method: 'Profiler.stopTypeProfile', callback?: (err: Error | null) => void): void; - /** - * Collect type profile. - * @experimental - */ - post( - method: 'Profiler.takeTypeProfile', - callback?: (err: Error | null, params: Profiler.TakeTypeProfileReturnType) => void, - ): void; - post(method: 'HeapProfiler.enable', callback?: (err: Error | null) => void): void; - post(method: 'HeapProfiler.disable', callback?: (err: Error | null) => void): void; - post( - method: 'HeapProfiler.startTrackingHeapObjects', - params?: HeapProfiler.StartTrackingHeapObjectsParameterType, - callback?: (err: Error | null) => void, - ): void; - post(method: 'HeapProfiler.startTrackingHeapObjects', callback?: (err: Error | null) => void): void; - post( - method: 'HeapProfiler.stopTrackingHeapObjects', - params?: HeapProfiler.StopTrackingHeapObjectsParameterType, - callback?: (err: Error | null) => void, - ): void; - post(method: 'HeapProfiler.stopTrackingHeapObjects', callback?: (err: Error | null) => void): void; - post( - method: 'HeapProfiler.takeHeapSnapshot', - params?: HeapProfiler.TakeHeapSnapshotParameterType, - callback?: (err: Error | null) => void, - ): void; - post(method: 'HeapProfiler.takeHeapSnapshot', callback?: (err: Error | null) => void): void; - post(method: 'HeapProfiler.collectGarbage', callback?: (err: Error | null) => void): void; - post( - method: 'HeapProfiler.getObjectByHeapObjectId', - params?: HeapProfiler.GetObjectByHeapObjectIdParameterType, - callback?: (err: Error | null, params: HeapProfiler.GetObjectByHeapObjectIdReturnType) => void, - ): void; - post( - method: 'HeapProfiler.getObjectByHeapObjectId', - callback?: (err: Error | null, params: HeapProfiler.GetObjectByHeapObjectIdReturnType) => void, - ): void; - /** - * Enables console to refer to the node with given id via $x (see Command Line API for more details $x functions). - */ - post( - method: 'HeapProfiler.addInspectedHeapObject', - params?: HeapProfiler.AddInspectedHeapObjectParameterType, - callback?: (err: Error | null) => void, - ): void; - post(method: 'HeapProfiler.addInspectedHeapObject', callback?: (err: Error | null) => void): void; - post( - method: 'HeapProfiler.getHeapObjectId', - params?: HeapProfiler.GetHeapObjectIdParameterType, - callback?: (err: Error | null, params: HeapProfiler.GetHeapObjectIdReturnType) => void, - ): void; - post( - method: 'HeapProfiler.getHeapObjectId', - callback?: (err: Error | null, params: HeapProfiler.GetHeapObjectIdReturnType) => void, - ): void; - post( - method: 'HeapProfiler.startSampling', - params?: HeapProfiler.StartSamplingParameterType, - callback?: (err: Error | null) => void, - ): void; - post(method: 'HeapProfiler.startSampling', callback?: (err: Error | null) => void): void; - post( - method: 'HeapProfiler.stopSampling', - callback?: (err: Error | null, params: HeapProfiler.StopSamplingReturnType) => void, - ): void; - post( - method: 'HeapProfiler.getSamplingProfile', - callback?: (err: Error | null, params: HeapProfiler.GetSamplingProfileReturnType) => void, - ): void; - /** - * Gets supported tracing categories. - */ - post( - method: 'NodeTracing.getCategories', - callback?: (err: Error | null, params: NodeTracing.GetCategoriesReturnType) => void, - ): void; - /** - * Start trace events collection. - */ - post( - method: 'NodeTracing.start', - params?: NodeTracing.StartParameterType, - callback?: (err: Error | null) => void, - ): void; - post(method: 'NodeTracing.start', callback?: (err: Error | null) => void): void; - /** - * Stop trace events collection. Remaining collected events will be sent as a sequence of - * dataCollected events followed by tracingComplete event. - */ - post(method: 'NodeTracing.stop', callback?: (err: Error | null) => void): void; - /** - * Sends protocol message over session with given id. - */ - post( - method: 'NodeWorker.sendMessageToWorker', - params?: NodeWorker.SendMessageToWorkerParameterType, - callback?: (err: Error | null) => void, - ): void; - post(method: 'NodeWorker.sendMessageToWorker', callback?: (err: Error | null) => void): void; - /** - * Instructs the inspector to attach to running workers. Will also attach to new workers - * as they start - */ - post( - method: 'NodeWorker.enable', - params?: NodeWorker.EnableParameterType, - callback?: (err: Error | null) => void, - ): void; - post(method: 'NodeWorker.enable', callback?: (err: Error | null) => void): void; - /** - * Detaches from all running workers and disables attaching to new workers as they are started. - */ - post(method: 'NodeWorker.disable', callback?: (err: Error | null) => void): void; - /** - * Detached from the worker with given sessionId. - */ - post( - method: 'NodeWorker.detach', - params?: NodeWorker.DetachParameterType, - callback?: (err: Error | null) => void, - ): void; - post(method: 'NodeWorker.detach', callback?: (err: Error | null) => void): void; - /** - * Enable the `NodeRuntime.waitingForDisconnect`. - */ - post( - method: 'NodeRuntime.notifyWhenWaitingForDisconnect', - params?: NodeRuntime.NotifyWhenWaitingForDisconnectParameterType, - callback?: (err: Error | null) => void, - ): void; - post(method: 'NodeRuntime.notifyWhenWaitingForDisconnect', callback?: (err: Error | null) => void): void; - // Events - addListener(event: string, listener: (...args: any[]) => void): this; - /** - * Emitted when any notification from the V8 Inspector is received. - */ - addListener(event: 'inspectorNotification', listener: (message: InspectorNotification<{}>) => void): this; - /** - * Issued when new execution context is created. - */ - addListener( - event: 'Runtime.executionContextCreated', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Issued when execution context is destroyed. - */ - addListener( - event: 'Runtime.executionContextDestroyed', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Issued when all executionContexts were cleared in browser - */ - addListener(event: 'Runtime.executionContextsCleared', listener: () => void): this; - /** - * Issued when exception was thrown and unhandled. - */ - addListener( - event: 'Runtime.exceptionThrown', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Issued when unhandled exception was revoked. - */ - addListener( - event: 'Runtime.exceptionRevoked', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Issued when console API was called. - */ - addListener( - event: 'Runtime.consoleAPICalled', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Issued when object should be inspected (for example, as a result of inspect() command line API call). - */ - addListener( - event: 'Runtime.inspectRequested', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Fired when virtual machine parses script. This event is also fired for all known and uncollected scripts upon enabling debugger. - */ - addListener( - event: 'Debugger.scriptParsed', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Fired when virtual machine fails to parse the script. - */ - addListener( - event: 'Debugger.scriptFailedToParse', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Fired when breakpoint is resolved to an actual script and location. - */ - addListener( - event: 'Debugger.breakpointResolved', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria. - */ - addListener( - event: 'Debugger.paused', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Fired when the virtual machine resumed execution. - */ - addListener(event: 'Debugger.resumed', listener: () => void): this; - /** - * Issued when new console message is added. - */ - addListener( - event: 'Console.messageAdded', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Sent when new profile recording is started using console.profile() call. - */ - addListener( - event: 'Profiler.consoleProfileStarted', - listener: (message: InspectorNotification) => void, - ): this; - addListener( - event: 'Profiler.consoleProfileFinished', - listener: (message: InspectorNotification) => void, - ): this; - addListener( - event: 'HeapProfiler.addHeapSnapshotChunk', - listener: (message: InspectorNotification) => void, - ): this; - addListener(event: 'HeapProfiler.resetProfiles', listener: () => void): this; - addListener( - event: 'HeapProfiler.reportHeapSnapshotProgress', - listener: (message: InspectorNotification) => void, - ): this; - /** - * If heap objects tracking has been started then backend regularly sends a current value for last seen object id and corresponding timestamp. If the were changes in the heap since last event then one or more heapStatsUpdate events will be sent before a new lastSeenObjectId event. - */ - addListener( - event: 'HeapProfiler.lastSeenObjectId', - listener: (message: InspectorNotification) => void, - ): this; - /** - * If heap objects tracking has been started then backend may send update for one or more fragments - */ - addListener( - event: 'HeapProfiler.heapStatsUpdate', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Contains an bucket of collected trace events. - */ - addListener( - event: 'NodeTracing.dataCollected', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Signals that tracing is stopped and there is no trace buffers pending flush, all data were - * delivered via dataCollected events. - */ - addListener(event: 'NodeTracing.tracingComplete', listener: () => void): this; - /** - * Issued when attached to a worker. - */ - addListener( - event: 'NodeWorker.attachedToWorker', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Issued when detached from the worker. - */ - addListener( - event: 'NodeWorker.detachedFromWorker', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Notifies about a new protocol message received from the session - * (session ID is provided in attachedToWorker notification). - */ - addListener( - event: 'NodeWorker.receivedMessageFromWorker', - listener: (message: InspectorNotification) => void, - ): this; - /** - * This event is fired instead of `Runtime.executionContextDestroyed` when - * enabled. - * It is fired when the Node process finished all code execution and is - * waiting for all frontends to disconnect. - */ - addListener(event: 'NodeRuntime.waitingForDisconnect', listener: () => void): this; - emit(event: string | symbol, ...args: any[]): boolean; - emit(event: 'inspectorNotification', message: InspectorNotification<{}>): boolean; - emit( - event: 'Runtime.executionContextCreated', - message: InspectorNotification, - ): boolean; - emit( - event: 'Runtime.executionContextDestroyed', - message: InspectorNotification, - ): boolean; - emit(event: 'Runtime.executionContextsCleared'): boolean; - emit( - event: 'Runtime.exceptionThrown', - message: InspectorNotification, - ): boolean; - emit( - event: 'Runtime.exceptionRevoked', - message: InspectorNotification, - ): boolean; - emit( - event: 'Runtime.consoleAPICalled', - message: InspectorNotification, - ): boolean; - emit( - event: 'Runtime.inspectRequested', - message: InspectorNotification, - ): boolean; - emit(event: 'Debugger.scriptParsed', message: InspectorNotification): boolean; - emit( - event: 'Debugger.scriptFailedToParse', - message: InspectorNotification, - ): boolean; - emit( - event: 'Debugger.breakpointResolved', - message: InspectorNotification, - ): boolean; - emit(event: 'Debugger.paused', message: InspectorNotification): boolean; - emit(event: 'Debugger.resumed'): boolean; - emit(event: 'Console.messageAdded', message: InspectorNotification): boolean; - emit( - event: 'Profiler.consoleProfileStarted', - message: InspectorNotification, - ): boolean; - emit( - event: 'Profiler.consoleProfileFinished', - message: InspectorNotification, - ): boolean; - emit( - event: 'HeapProfiler.addHeapSnapshotChunk', - message: InspectorNotification, - ): boolean; - emit(event: 'HeapProfiler.resetProfiles'): boolean; - emit( - event: 'HeapProfiler.reportHeapSnapshotProgress', - message: InspectorNotification, - ): boolean; - emit( - event: 'HeapProfiler.lastSeenObjectId', - message: InspectorNotification, - ): boolean; - emit( - event: 'HeapProfiler.heapStatsUpdate', - message: InspectorNotification, - ): boolean; - emit( - event: 'NodeTracing.dataCollected', - message: InspectorNotification, - ): boolean; - emit(event: 'NodeTracing.tracingComplete'): boolean; - emit( - event: 'NodeWorker.attachedToWorker', - message: InspectorNotification, - ): boolean; - emit( - event: 'NodeWorker.detachedFromWorker', - message: InspectorNotification, - ): boolean; - emit( - event: 'NodeWorker.receivedMessageFromWorker', - message: InspectorNotification, - ): boolean; - emit(event: 'NodeRuntime.waitingForDisconnect'): boolean; - on(event: string, listener: (...args: any[]) => void): this; - /** - * Emitted when any notification from the V8 Inspector is received. - */ - on(event: 'inspectorNotification', listener: (message: InspectorNotification<{}>) => void): this; - /** - * Issued when new execution context is created. - */ - on( - event: 'Runtime.executionContextCreated', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Issued when execution context is destroyed. - */ - on( - event: 'Runtime.executionContextDestroyed', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Issued when all executionContexts were cleared in browser - */ - on(event: 'Runtime.executionContextsCleared', listener: () => void): this; - /** - * Issued when exception was thrown and unhandled. - */ - on( - event: 'Runtime.exceptionThrown', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Issued when unhandled exception was revoked. - */ - on( - event: 'Runtime.exceptionRevoked', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Issued when console API was called. - */ - on( - event: 'Runtime.consoleAPICalled', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Issued when object should be inspected (for example, as a result of inspect() command line API call). - */ - on( - event: 'Runtime.inspectRequested', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Fired when virtual machine parses script. This event is also fired for all known and uncollected scripts upon enabling debugger. - */ - on( - event: 'Debugger.scriptParsed', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Fired when virtual machine fails to parse the script. - */ - on( - event: 'Debugger.scriptFailedToParse', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Fired when breakpoint is resolved to an actual script and location. - */ - on( - event: 'Debugger.breakpointResolved', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria. - */ - on( - event: 'Debugger.paused', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Fired when the virtual machine resumed execution. - */ - on(event: 'Debugger.resumed', listener: () => void): this; - /** - * Issued when new console message is added. - */ - on( - event: 'Console.messageAdded', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Sent when new profile recording is started using console.profile() call. - */ - on( - event: 'Profiler.consoleProfileStarted', - listener: (message: InspectorNotification) => void, - ): this; - on( - event: 'Profiler.consoleProfileFinished', - listener: (message: InspectorNotification) => void, - ): this; - on( - event: 'HeapProfiler.addHeapSnapshotChunk', - listener: (message: InspectorNotification) => void, - ): this; - on(event: 'HeapProfiler.resetProfiles', listener: () => void): this; - on( - event: 'HeapProfiler.reportHeapSnapshotProgress', - listener: (message: InspectorNotification) => void, - ): this; - /** - * If heap objects tracking has been started then backend regularly sends a current value for last seen object id and corresponding timestamp. If the were changes in the heap since last event then one or more heapStatsUpdate events will be sent before a new lastSeenObjectId event. - */ - on( - event: 'HeapProfiler.lastSeenObjectId', - listener: (message: InspectorNotification) => void, - ): this; - /** - * If heap objects tracking has been started then backend may send update for one or more fragments - */ - on( - event: 'HeapProfiler.heapStatsUpdate', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Contains an bucket of collected trace events. - */ - on( - event: 'NodeTracing.dataCollected', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Signals that tracing is stopped and there is no trace buffers pending flush, all data were - * delivered via dataCollected events. - */ - on(event: 'NodeTracing.tracingComplete', listener: () => void): this; - /** - * Issued when attached to a worker. - */ - on( - event: 'NodeWorker.attachedToWorker', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Issued when detached from the worker. - */ - on( - event: 'NodeWorker.detachedFromWorker', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Notifies about a new protocol message received from the session - * (session ID is provided in attachedToWorker notification). - */ - on( - event: 'NodeWorker.receivedMessageFromWorker', - listener: (message: InspectorNotification) => void, - ): this; - /** - * This event is fired instead of `Runtime.executionContextDestroyed` when - * enabled. - * It is fired when the Node process finished all code execution and is - * waiting for all frontends to disconnect. - */ - on(event: 'NodeRuntime.waitingForDisconnect', listener: () => void): this; - once(event: string, listener: (...args: any[]) => void): this; - /** - * Emitted when any notification from the V8 Inspector is received. - */ - once(event: 'inspectorNotification', listener: (message: InspectorNotification<{}>) => void): this; - /** - * Issued when new execution context is created. - */ - once( - event: 'Runtime.executionContextCreated', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Issued when execution context is destroyed. - */ - once( - event: 'Runtime.executionContextDestroyed', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Issued when all executionContexts were cleared in browser - */ - once(event: 'Runtime.executionContextsCleared', listener: () => void): this; - /** - * Issued when exception was thrown and unhandled. - */ - once( - event: 'Runtime.exceptionThrown', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Issued when unhandled exception was revoked. - */ - once( - event: 'Runtime.exceptionRevoked', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Issued when console API was called. - */ - once( - event: 'Runtime.consoleAPICalled', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Issued when object should be inspected (for example, as a result of inspect() command line API call). - */ - once( - event: 'Runtime.inspectRequested', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Fired when virtual machine parses script. This event is also fired for all known and uncollected scripts upon enabling debugger. - */ - once( - event: 'Debugger.scriptParsed', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Fired when virtual machine fails to parse the script. - */ - once( - event: 'Debugger.scriptFailedToParse', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Fired when breakpoint is resolved to an actual script and location. - */ - once( - event: 'Debugger.breakpointResolved', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria. - */ - once( - event: 'Debugger.paused', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Fired when the virtual machine resumed execution. - */ - once(event: 'Debugger.resumed', listener: () => void): this; - /** - * Issued when new console message is added. - */ - once( - event: 'Console.messageAdded', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Sent when new profile recording is started using console.profile() call. - */ - once( - event: 'Profiler.consoleProfileStarted', - listener: (message: InspectorNotification) => void, - ): this; - once( - event: 'Profiler.consoleProfileFinished', - listener: (message: InspectorNotification) => void, - ): this; - once( - event: 'HeapProfiler.addHeapSnapshotChunk', - listener: (message: InspectorNotification) => void, - ): this; - once(event: 'HeapProfiler.resetProfiles', listener: () => void): this; - once( - event: 'HeapProfiler.reportHeapSnapshotProgress', - listener: (message: InspectorNotification) => void, - ): this; - /** - * If heap objects tracking has been started then backend regularly sends a current value for last seen object id and corresponding timestamp. If the were changes in the heap since last event then one or more heapStatsUpdate events will be sent before a new lastSeenObjectId event. - */ - once( - event: 'HeapProfiler.lastSeenObjectId', - listener: (message: InspectorNotification) => void, - ): this; - /** - * If heap objects tracking has been started then backend may send update for one or more fragments - */ - once( - event: 'HeapProfiler.heapStatsUpdate', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Contains an bucket of collected trace events. - */ - once( - event: 'NodeTracing.dataCollected', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Signals that tracing is stopped and there is no trace buffers pending flush, all data were - * delivered via dataCollected events. - */ - once(event: 'NodeTracing.tracingComplete', listener: () => void): this; - /** - * Issued when attached to a worker. - */ - once( - event: 'NodeWorker.attachedToWorker', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Issued when detached from the worker. - */ - once( - event: 'NodeWorker.detachedFromWorker', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Notifies about a new protocol message received from the session - * (session ID is provided in attachedToWorker notification). - */ - once( - event: 'NodeWorker.receivedMessageFromWorker', - listener: (message: InspectorNotification) => void, - ): this; - /** - * This event is fired instead of `Runtime.executionContextDestroyed` when - * enabled. - * It is fired when the Node process finished all code execution and is - * waiting for all frontends to disconnect. - */ - once(event: 'NodeRuntime.waitingForDisconnect', listener: () => void): this; - prependListener(event: string, listener: (...args: any[]) => void): this; - /** - * Emitted when any notification from the V8 Inspector is received. - */ - prependListener(event: 'inspectorNotification', listener: (message: InspectorNotification<{}>) => void): this; - /** - * Issued when new execution context is created. - */ - prependListener( - event: 'Runtime.executionContextCreated', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Issued when execution context is destroyed. - */ - prependListener( - event: 'Runtime.executionContextDestroyed', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Issued when all executionContexts were cleared in browser - */ - prependListener(event: 'Runtime.executionContextsCleared', listener: () => void): this; - /** - * Issued when exception was thrown and unhandled. - */ - prependListener( - event: 'Runtime.exceptionThrown', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Issued when unhandled exception was revoked. - */ - prependListener( - event: 'Runtime.exceptionRevoked', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Issued when console API was called. - */ - prependListener( - event: 'Runtime.consoleAPICalled', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Issued when object should be inspected (for example, as a result of inspect() command line API call). - */ - prependListener( - event: 'Runtime.inspectRequested', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Fired when virtual machine parses script. This event is also fired for all known and uncollected scripts upon enabling debugger. - */ - prependListener( - event: 'Debugger.scriptParsed', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Fired when virtual machine fails to parse the script. - */ - prependListener( - event: 'Debugger.scriptFailedToParse', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Fired when breakpoint is resolved to an actual script and location. - */ - prependListener( - event: 'Debugger.breakpointResolved', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria. - */ - prependListener( - event: 'Debugger.paused', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Fired when the virtual machine resumed execution. - */ - prependListener(event: 'Debugger.resumed', listener: () => void): this; - /** - * Issued when new console message is added. - */ - prependListener( - event: 'Console.messageAdded', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Sent when new profile recording is started using console.profile() call. - */ - prependListener( - event: 'Profiler.consoleProfileStarted', - listener: (message: InspectorNotification) => void, - ): this; - prependListener( - event: 'Profiler.consoleProfileFinished', - listener: (message: InspectorNotification) => void, - ): this; - prependListener( - event: 'HeapProfiler.addHeapSnapshotChunk', - listener: (message: InspectorNotification) => void, - ): this; - prependListener(event: 'HeapProfiler.resetProfiles', listener: () => void): this; - prependListener( - event: 'HeapProfiler.reportHeapSnapshotProgress', - listener: (message: InspectorNotification) => void, - ): this; - /** - * If heap objects tracking has been started then backend regularly sends a current value for last seen object id and corresponding timestamp. If the were changes in the heap since last event then one or more heapStatsUpdate events will be sent before a new lastSeenObjectId event. - */ - prependListener( - event: 'HeapProfiler.lastSeenObjectId', - listener: (message: InspectorNotification) => void, - ): this; - /** - * If heap objects tracking has been started then backend may send update for one or more fragments - */ - prependListener( - event: 'HeapProfiler.heapStatsUpdate', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Contains an bucket of collected trace events. - */ - prependListener( - event: 'NodeTracing.dataCollected', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Signals that tracing is stopped and there is no trace buffers pending flush, all data were - * delivered via dataCollected events. - */ - prependListener(event: 'NodeTracing.tracingComplete', listener: () => void): this; - /** - * Issued when attached to a worker. - */ - prependListener( - event: 'NodeWorker.attachedToWorker', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Issued when detached from the worker. - */ - prependListener( - event: 'NodeWorker.detachedFromWorker', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Notifies about a new protocol message received from the session - * (session ID is provided in attachedToWorker notification). - */ - prependListener( - event: 'NodeWorker.receivedMessageFromWorker', - listener: (message: InspectorNotification) => void, - ): this; - /** - * This event is fired instead of `Runtime.executionContextDestroyed` when - * enabled. - * It is fired when the Node process finished all code execution and is - * waiting for all frontends to disconnect. - */ - prependListener(event: 'NodeRuntime.waitingForDisconnect', listener: () => void): this; - prependOnceListener(event: string, listener: (...args: any[]) => void): this; - /** - * Emitted when any notification from the V8 Inspector is received. - */ - prependOnceListener(event: 'inspectorNotification', listener: (message: InspectorNotification<{}>) => void): this; - /** - * Issued when new execution context is created. - */ - prependOnceListener( - event: 'Runtime.executionContextCreated', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Issued when execution context is destroyed. - */ - prependOnceListener( - event: 'Runtime.executionContextDestroyed', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Issued when all executionContexts were cleared in browser - */ - prependOnceListener(event: 'Runtime.executionContextsCleared', listener: () => void): this; - /** - * Issued when exception was thrown and unhandled. - */ - prependOnceListener( - event: 'Runtime.exceptionThrown', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Issued when unhandled exception was revoked. - */ - prependOnceListener( - event: 'Runtime.exceptionRevoked', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Issued when console API was called. - */ - prependOnceListener( - event: 'Runtime.consoleAPICalled', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Issued when object should be inspected (for example, as a result of inspect() command line API call). - */ - prependOnceListener( - event: 'Runtime.inspectRequested', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Fired when virtual machine parses script. This event is also fired for all known and uncollected scripts upon enabling debugger. - */ - prependOnceListener( - event: 'Debugger.scriptParsed', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Fired when virtual machine fails to parse the script. - */ - prependOnceListener( - event: 'Debugger.scriptFailedToParse', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Fired when breakpoint is resolved to an actual script and location. - */ - prependOnceListener( - event: 'Debugger.breakpointResolved', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria. - */ - prependOnceListener( - event: 'Debugger.paused', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Fired when the virtual machine resumed execution. - */ - prependOnceListener(event: 'Debugger.resumed', listener: () => void): this; - /** - * Issued when new console message is added. - */ - prependOnceListener( - event: 'Console.messageAdded', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Sent when new profile recording is started using console.profile() call. - */ - prependOnceListener( - event: 'Profiler.consoleProfileStarted', - listener: (message: InspectorNotification) => void, - ): this; - prependOnceListener( - event: 'Profiler.consoleProfileFinished', - listener: (message: InspectorNotification) => void, - ): this; - prependOnceListener( - event: 'HeapProfiler.addHeapSnapshotChunk', - listener: (message: InspectorNotification) => void, - ): this; - prependOnceListener(event: 'HeapProfiler.resetProfiles', listener: () => void): this; - prependOnceListener( - event: 'HeapProfiler.reportHeapSnapshotProgress', - listener: (message: InspectorNotification) => void, - ): this; - /** - * If heap objects tracking has been started then backend regularly sends a current value for last seen object id and corresponding timestamp. If the were changes in the heap since last event then one or more heapStatsUpdate events will be sent before a new lastSeenObjectId event. - */ - prependOnceListener( - event: 'HeapProfiler.lastSeenObjectId', - listener: (message: InspectorNotification) => void, - ): this; - /** - * If heap objects tracking has been started then backend may send update for one or more fragments - */ - prependOnceListener( - event: 'HeapProfiler.heapStatsUpdate', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Contains an bucket of collected trace events. - */ - prependOnceListener( - event: 'NodeTracing.dataCollected', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Signals that tracing is stopped and there is no trace buffers pending flush, all data were - * delivered via dataCollected events. - */ - prependOnceListener(event: 'NodeTracing.tracingComplete', listener: () => void): this; - /** - * Issued when attached to a worker. - */ - prependOnceListener( - event: 'NodeWorker.attachedToWorker', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Issued when detached from the worker. - */ - prependOnceListener( - event: 'NodeWorker.detachedFromWorker', - listener: (message: InspectorNotification) => void, - ): this; - /** - * Notifies about a new protocol message received from the session - * (session ID is provided in attachedToWorker notification). - */ - prependOnceListener( - event: 'NodeWorker.receivedMessageFromWorker', - listener: (message: InspectorNotification) => void, - ): this; - /** - * This event is fired instead of `Runtime.executionContextDestroyed` when - * enabled. - * It is fired when the Node process finished all code execution and is - * waiting for all frontends to disconnect. - */ - prependOnceListener(event: 'NodeRuntime.waitingForDisconnect', listener: () => void): this; - } - /** - * Activate inspector on host and port. Equivalent to`node --inspect=[[host:]port]`, but can be done programmatically after node has - * started. - * - * If wait is `true`, will block until a client has connected to the inspect port - * and flow control has been passed to the debugger client. - * - * See the `security warning` regarding the `host`parameter usage. - * @param [port='what was specified on the CLI'] Port to listen on for inspector connections. Optional. - * @param [host='what was specified on the CLI'] Host to listen on for inspector connections. Optional. - * @param [wait=false] Block until a client has connected. Optional. - */ - function open(port?: number, host?: string, wait?: boolean): void; - /** - * Deactivate the inspector. Blocks until there are no active connections. - */ - function close(): void; - /** - * Return the URL of the active inspector, or `undefined` if there is none. - * - * ```console - * $ node --inspect -p 'inspector.url()' - * Debugger listening on ws://127.0.0.1:9229/166e272e-7a30-4d09-97ce-f1c012b43c34 - * For help, see: https://nodejs.org/en/docs/inspector - * ws://127.0.0.1:9229/166e272e-7a30-4d09-97ce-f1c012b43c34 - * - * $ node --inspect=localhost:3000 -p 'inspector.url()' - * Debugger listening on ws://localhost:3000/51cf8d0e-3c36-4c59-8efd-54519839e56a - * For help, see: https://nodejs.org/en/docs/inspector - * ws://localhost:3000/51cf8d0e-3c36-4c59-8efd-54519839e56a - * - * $ node -p 'inspector.url()' - * undefined - * ``` - */ - function url(): string | undefined; - /** - * Blocks until a client (existing or connected later) has sent`Runtime.runIfWaitingForDebugger` command. - * - * An exception will be thrown if there is no active inspector. - * @since v12.7.0 - */ - function waitForDebugger(): void; -} -/** - * The inspector module provides an API for interacting with the V8 inspector. - */ -declare module 'node:inspector' { - import inspector = require('inspector'); - export = inspector; -} - -/** - * @types/node doesn't have a `node:inspector/promises` module, maybe because it's still experimental? - */ -declare module 'node:inspector/promises' { - /** - * Async Debugger session - */ - class Session { - constructor(); - - connect(): void; - - post(method: 'Debugger.pause' | 'Debugger.resume' | 'Debugger.enable' | 'Debugger.disable'): Promise; - post(method: 'Debugger.setPauseOnExceptions', params: Debugger.SetPauseOnExceptionsParameterType): Promise; - post( - method: 'Runtime.getProperties', - params: Runtime.GetPropertiesParameterType, - ): Promise; - - on( - event: 'Debugger.paused', - listener: (message: InspectorNotification) => void, - ): Session; - - on(event: 'Debugger.resumed', listener: () => void): Session; - } -} diff --git a/packages/node/src/integrations/local-variables/local-variables-async.ts b/packages/node/src/integrations/local-variables/local-variables-async.ts deleted file mode 100644 index fee8937af91d..000000000000 --- a/packages/node/src/integrations/local-variables/local-variables-async.ts +++ /dev/null @@ -1,277 +0,0 @@ -import type { Session } from 'node:inspector/promises'; -import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; -import type { Event, Exception, Integration, IntegrationClass, IntegrationFn, StackParser } from '@sentry/types'; -import { LRUMap, dynamicRequire, logger } from '@sentry/utils'; -import type { Debugger, InspectorNotification, Runtime } from 'inspector'; - -import type { NodeClient } from '../../client'; -import type { NodeClientOptions } from '../../types'; -import type { - FrameVariables, - LocalVariablesIntegrationOptions, - PausedExceptionEvent, - RateLimitIncrement, - Variables, -} from './common'; -import { createRateLimiter, functionNamesMatch, hashFrames, hashFromStack } from './common'; - -async function unrollArray(session: Session, objectId: string, name: string, vars: Variables): Promise { - const properties: Runtime.GetPropertiesReturnType = await session.post('Runtime.getProperties', { - objectId, - ownProperties: true, - }); - - vars[name] = properties.result - .filter(v => v.name !== 'length' && !isNaN(parseInt(v.name, 10))) - .sort((a, b) => parseInt(a.name, 10) - parseInt(b.name, 10)) - .map(v => v.value?.value); -} - -async function unrollObject(session: Session, objectId: string, name: string, vars: Variables): Promise { - const properties: Runtime.GetPropertiesReturnType = await session.post('Runtime.getProperties', { - objectId, - ownProperties: true, - }); - - vars[name] = properties.result - .map<[string, unknown]>(v => [v.name, v.value?.value]) - .reduce((obj, [key, val]) => { - obj[key] = val; - return obj; - }, {} as Variables); -} - -function unrollOther(prop: Runtime.PropertyDescriptor, vars: Variables): void { - if (!prop.value) { - return; - } - - if ('value' in prop.value) { - if (prop.value.value === undefined || prop.value.value === null) { - vars[prop.name] = `<${prop.value.value}>`; - } else { - vars[prop.name] = prop.value.value; - } - } else if ('description' in prop.value && prop.value.type !== 'function') { - vars[prop.name] = `<${prop.value.description}>`; - } else if (prop.value.type === 'undefined') { - vars[prop.name] = ''; - } -} - -async function getLocalVariables(session: Session, objectId: string): Promise { - const properties: Runtime.GetPropertiesReturnType = await session.post('Runtime.getProperties', { - objectId, - ownProperties: true, - }); - const variables = {}; - - for (const prop of properties.result) { - if (prop?.value?.objectId && prop?.value.className === 'Array') { - const id = prop.value.objectId; - await unrollArray(session, id, prop.name, variables); - } else if (prop?.value?.objectId && prop?.value?.className === 'Object') { - const id = prop.value.objectId; - await unrollObject(session, id, prop.name, variables); - } else if (prop?.value) { - unrollOther(prop, variables); - } - } - - return variables; -} - -const INTEGRATION_NAME = 'LocalVariablesAsync'; - -/** - * Adds local variables to exception frames - */ -const _localVariablesAsyncIntegration = ((options: LocalVariablesIntegrationOptions = {}) => { - const cachedFrames: LRUMap = new LRUMap(20); - let rateLimiter: RateLimitIncrement | undefined; - let shouldProcessEvent = false; - - async function handlePaused( - session: Session, - stackParser: StackParser, - { reason, data, callFrames }: PausedExceptionEvent, - ): Promise { - if (reason !== 'exception' && reason !== 'promiseRejection') { - return; - } - - rateLimiter?.(); - - // data.description contains the original error.stack - const exceptionHash = hashFromStack(stackParser, data?.description); - - if (exceptionHash == undefined) { - return; - } - - const frames = []; - - for (let i = 0; i < callFrames.length; i++) { - const { scopeChain, functionName, this: obj } = callFrames[i]; - - const localScope = scopeChain.find(scope => scope.type === 'local'); - - // obj.className is undefined in ESM modules - const fn = obj.className === 'global' || !obj.className ? functionName : `${obj.className}.${functionName}`; - - if (localScope?.object.objectId === undefined) { - frames[i] = { function: fn }; - } else { - const vars = await getLocalVariables(session, localScope.object.objectId); - frames[i] = { function: fn, vars }; - } - } - - cachedFrames.set(exceptionHash, frames); - } - - async function startDebugger(session: Session, clientOptions: NodeClientOptions): Promise { - session.connect(); - - let isPaused = false; - - session.on('Debugger.resumed', () => { - isPaused = false; - }); - - session.on('Debugger.paused', (event: InspectorNotification) => { - isPaused = true; - - handlePaused(session, clientOptions.stackParser, event.params as PausedExceptionEvent).then( - () => { - // After the pause work is complete, resume execution! - return isPaused ? session.post('Debugger.resume') : Promise.resolve(); - }, - _ => { - // ignore - }, - ); - }); - - await session.post('Debugger.enable'); - - const captureAll = options.captureAllExceptions !== false; - await session.post('Debugger.setPauseOnExceptions', { state: captureAll ? 'all' : 'uncaught' }); - - if (captureAll) { - const max = options.maxExceptionsPerSecond || 50; - - rateLimiter = createRateLimiter( - max, - () => { - logger.log('Local variables rate-limit lifted.'); - return session.post('Debugger.setPauseOnExceptions', { state: 'all' }); - }, - seconds => { - logger.log( - `Local variables rate-limit exceeded. Disabling capturing of caught exceptions for ${seconds} seconds.`, - ); - return session.post('Debugger.setPauseOnExceptions', { state: 'uncaught' }); - }, - ); - } - - shouldProcessEvent = true; - } - - function addLocalVariablesToException(exception: Exception): void { - const hash = hashFrames(exception.stacktrace?.frames); - - if (hash === undefined) { - return; - } - - // Check if we have local variables for an exception that matches the hash - // remove is identical to get but also removes the entry from the cache - const cachedFrame = cachedFrames.remove(hash); - - if (cachedFrame === undefined) { - return; - } - - const frameCount = exception.stacktrace?.frames?.length || 0; - - for (let i = 0; i < frameCount; i++) { - // Sentry frames are in reverse order - const frameIndex = frameCount - i - 1; - - // Drop out if we run out of frames to match up - if (!exception.stacktrace?.frames?.[frameIndex] || !cachedFrame[i]) { - break; - } - - if ( - // We need to have vars to add - cachedFrame[i].vars === undefined || - // We're not interested in frames that are not in_app because the vars are not relevant - exception.stacktrace.frames[frameIndex].in_app === false || - // The function names need to match - !functionNamesMatch(exception.stacktrace.frames[frameIndex].function, cachedFrame[i].function) - ) { - continue; - } - - exception.stacktrace.frames[frameIndex].vars = cachedFrame[i].vars; - } - } - - function addLocalVariablesToEvent(event: Event): Event { - for (const exception of event.exception?.values || []) { - addLocalVariablesToException(exception); - } - - return event; - } - - return { - name: INTEGRATION_NAME, - setup(client: NodeClient) { - const clientOptions = client.getOptions(); - - if (!clientOptions.includeLocalVariables) { - return; - } - - try { - // TODO: Use import()... - // It would be nice to use import() here, but this built-in library is not in Node <19 so webpack will pick it - // up and report it as a missing dependency - const { Session } = dynamicRequire(module, 'node:inspector/promises'); - - startDebugger(new Session(), clientOptions).catch(e => { - logger.error('Failed to start inspector session', e); - }); - } catch (e) { - logger.error('Failed to load inspector API', e); - return; - } - }, - processEvent(event: Event): Event { - if (shouldProcessEvent) { - return addLocalVariablesToEvent(event); - } - - return event; - }, - }; -}) satisfies IntegrationFn; - -export const localVariablesAsyncIntegration = defineIntegration(_localVariablesAsyncIntegration); - -/** - * Adds local variables to exception frames. - * @deprecated Use `localVariablesAsyncIntegration()` instead. - */ -// eslint-disable-next-line deprecation/deprecation -export const LocalVariablesAsync = convertIntegrationFnToClass( - INTEGRATION_NAME, - localVariablesAsyncIntegration, -) as IntegrationClass Event; setup: (client: NodeClient) => void }>; - -// eslint-disable-next-line deprecation/deprecation -export type LocalVariablesAsync = typeof LocalVariablesAsync; diff --git a/packages/node/src/integrations/local-variables/local-variables-sync.ts b/packages/node/src/integrations/local-variables/local-variables-sync.ts index e1ec4b57023c..91fb9005b4c3 100644 --- a/packages/node/src/integrations/local-variables/local-variables-sync.ts +++ b/packages/node/src/integrations/local-variables/local-variables-sync.ts @@ -1,11 +1,11 @@ -/* eslint-disable max-lines */ -import { convertIntegrationFnToClass, defineIntegration, getClient } from '@sentry/core'; -import type { Event, Exception, Integration, IntegrationClass, IntegrationFn, StackParser } from '@sentry/types'; +import { defineIntegration, getClient } from '@sentry/core'; +import type { Event, Exception, IntegrationFn, StackParser } from '@sentry/types'; import { LRUMap, logger } from '@sentry/utils'; -import type { Debugger, InspectorNotification, Runtime, Session } from 'inspector'; -import type { NodeClient } from '../../client'; +import type { Debugger, InspectorNotification, Runtime } from 'inspector'; +import { Session } from 'inspector'; -import { NODE_VERSION } from '../../nodeVersion'; +import { NODE_MAJOR } from '../../nodeVersion'; +import type { NodeClient } from '../../sdk/client'; import type { FrameVariables, LocalVariablesIntegrationOptions, @@ -79,22 +79,6 @@ class AsyncSession implements DebugSession { /** Throws if inspector API is not available */ public constructor() { - /* - TODO: We really should get rid of this require statement below for a couple of reasons: - 1. It makes the integration unusable in the SvelteKit SDK, as it's not possible to use `require` - in SvelteKit server code (at least not by default). - 2. Throwing in a constructor is bad practice - - More context for a future attempt to fix this: - We already tried replacing it with import but didn't get it to work because of async problems. - We still called import in the constructor but assigned to a promise which we "awaited" in - `configureAndConnect`. However, this broke the Node integration tests as no local variables - were reported any more. We probably missed a place where we need to await the promise, too. - */ - - // Node can be built without inspector support so this can throw - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { Session } = require('inspector'); this._session = new Session(); } @@ -304,14 +288,16 @@ const _localVariablesSyncIntegration = (( return; } - const frameCount = exception.stacktrace?.frames?.length || 0; + // Filter out frames where the function name is `new Promise` since these are in the error.stack frames + // but do not appear in the debugger call frames + const frames = (exception.stacktrace?.frames || []).filter(frame => frame.function !== 'new Promise'); - for (let i = 0; i < frameCount; i++) { + for (let i = 0; i < frames.length; i++) { // Sentry frames are in reverse order - const frameIndex = frameCount - i - 1; + const frameIndex = frames.length - i - 1; // Drop out if we run out of frames to match up - if (!exception?.stacktrace?.frames?.[frameIndex] || !cachedFrame[i]) { + if (!frames[frameIndex] || !cachedFrame[i]) { break; } @@ -319,14 +305,14 @@ const _localVariablesSyncIntegration = (( // We need to have vars to add cachedFrame[i].vars === undefined || // We're not interested in frames that are not in_app because the vars are not relevant - exception.stacktrace.frames[frameIndex].in_app === false || + frames[frameIndex].in_app === false || // The function names need to match - !functionNamesMatch(exception.stacktrace.frames[frameIndex].function, cachedFrame[i].function) + !functionNamesMatch(frames[frameIndex].function, cachedFrame[i].function) ) { continue; } - exception.stacktrace.frames[frameIndex].vars = cachedFrame[i].vars; + frames[frameIndex].vars = cachedFrame[i].vars; } } @@ -347,7 +333,7 @@ const _localVariablesSyncIntegration = (( if (session && clientOptions?.includeLocalVariables) { // Only setup this integration if the Node version is >= v18 // https://github.com/getsentry/sentry-javascript/issues/7697 - const unsupportedNodeVersion = NODE_VERSION.major < 18; + const unsupportedNodeVersion = NODE_MAJOR < 18; if (unsupportedNodeVersion) { logger.log('The `LocalVariables` integration is only supported on Node >= v18.'); @@ -400,19 +386,7 @@ const _localVariablesSyncIntegration = (( }; }) satisfies IntegrationFn; -export const localVariablesSyncIntegration = defineIntegration(_localVariablesSyncIntegration); - /** * Adds local variables to exception frames. - * @deprecated Use `localVariablesSyncIntegration()` instead. */ -// eslint-disable-next-line deprecation/deprecation -export const LocalVariablesSync = convertIntegrationFnToClass( - INTEGRATION_NAME, - localVariablesSyncIntegration, -) as IntegrationClass Event; setup: (client: NodeClient) => void }> & { - new (options?: LocalVariablesIntegrationOptions, session?: DebugSession): Integration; -}; - -// eslint-disable-next-line deprecation/deprecation -export type LocalVariablesSync = typeof LocalVariablesSync; +export const localVariablesSyncIntegration = defineIntegration(_localVariablesSyncIntegration); diff --git a/packages/node/src/integrations/modules.ts b/packages/node/src/integrations/modules.ts index 1f9aff7303e3..ad30bb4d7a3b 100644 --- a/packages/node/src/integrations/modules.ts +++ b/packages/node/src/integrations/modules.ts @@ -1,12 +1,31 @@ import { existsSync, readFileSync } from 'fs'; import { dirname, join } from 'path'; -import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; -import type { Event, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; +import { defineIntegration } from '@sentry/core'; +import type { IntegrationFn } from '@sentry/types'; let moduleCache: { [key: string]: string }; const INTEGRATION_NAME = 'Modules'; +const _modulesIntegration = (() => { + return { + name: INTEGRATION_NAME, + processEvent(event) { + event.modules = { + ...event.modules, + ..._getModules(), + }; + + return event; + }, + }; +}) satisfies IntegrationFn; + +/** + * Add node modules / packages to the event. + */ +export const modulesIntegration = defineIntegration(_modulesIntegration); + /** Extract information about paths */ function getPaths(): string[] { try { @@ -75,31 +94,3 @@ function _getModules(): { [key: string]: string } { } return moduleCache; } - -const _modulesIntegration = (() => { - return { - name: INTEGRATION_NAME, - processEvent(event) { - event.modules = { - ...event.modules, - ..._getModules(), - }; - - return event; - }, - }; -}) satisfies IntegrationFn; - -export const modulesIntegration = defineIntegration(_modulesIntegration); - -/** - * Add node modules / packages to the event. - * @deprecated Use `modulesIntegration()` instead. - */ -// eslint-disable-next-line deprecation/deprecation -export const Modules = convertIntegrationFnToClass(INTEGRATION_NAME, modulesIntegration) as IntegrationClass< - Integration & { processEvent: (event: Event) => Event } ->; - -// eslint-disable-next-line deprecation/deprecation -export type Modules = typeof Modules; diff --git a/packages/node-experimental/src/integrations/node-fetch.ts b/packages/node/src/integrations/node-fetch.ts similarity index 100% rename from packages/node-experimental/src/integrations/node-fetch.ts rename to packages/node/src/integrations/node-fetch.ts diff --git a/packages/node/src/integrations/onuncaughtexception.ts b/packages/node/src/integrations/onuncaughtexception.ts index 68be68a6d6cc..e56c3c0801d7 100644 --- a/packages/node/src/integrations/onuncaughtexception.ts +++ b/packages/node/src/integrations/onuncaughtexception.ts @@ -1,11 +1,11 @@ -import { captureException, convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; +import { captureException, defineIntegration } from '@sentry/core'; import { getClient } from '@sentry/core'; -import type { Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; +import type { IntegrationFn } from '@sentry/types'; import { logger } from '@sentry/utils'; -import type { NodeClient } from '../client'; import { DEBUG_BUILD } from '../debug-build'; -import { logAndExitProcess } from './utils/errorhandling'; +import type { NodeClient } from '../sdk/client'; +import { logAndExitProcess } from '../utils/errorhandling'; type OnFatalErrorHandler = (firstError: Error, secondError?: Error) => void; @@ -54,27 +54,10 @@ const _onUncaughtExceptionIntegration = ((options: Partial void }> & { - new ( - options?: Partial<{ - exitEvenIfOtherHandlersAreRegistered: boolean; - onFatalError?(this: void, firstError: Error, secondError?: Error): void; - }>, - ): Integration; -}; - -// eslint-disable-next-line deprecation/deprecation -export type OnUncaughtException = typeof OnUncaughtException; +export const onUncaughtExceptionIntegration = defineIntegration(_onUncaughtExceptionIntegration); type ErrorHandler = { _errorHandler: boolean } & ((error: Error) => void); @@ -103,20 +86,19 @@ export function makeErrorHandler(client: NodeClient, options: OnUncaughtExceptio // exit behaviour of the SDK accordingly: // - If other listeners are attached, do not exit. // - If the only listener attached is ours, exit. - const userProvidedListenersCount = ( - global.process.listeners('uncaughtException') as TaggedListener[] - ).reduce((acc, listener) => { - if ( + const userProvidedListenersCount = (global.process.listeners('uncaughtException') as TaggedListener[]).filter( + listener => { // There are 3 listeners we ignore: - listener.name === 'domainUncaughtExceptionClear' || // as soon as we're using domains this listener is attached by node itself - (listener.tag && listener.tag === 'sentry_tracingErrorCallback') || // the handler we register for tracing - (listener as ErrorHandler)._errorHandler // the handler we register in this integration - ) { - return acc; - } else { - return acc + 1; - } - }, 0); + return ( + // as soon as we're using domains this listener is attached by node itself + listener.name !== 'domainUncaughtExceptionClear' && + // the handler we register for tracing + listener.tag !== 'sentry_tracingErrorCallback' && + // the handler we register in this integration + (listener as ErrorHandler)._errorHandler !== true + ); + }, + ).length; const processWouldExit = userProvidedListenersCount === 0; const shouldApplyFatalHandlingLogic = options.exitEvenIfOtherHandlersAreRegistered || processWouldExit; diff --git a/packages/node/src/integrations/onunhandledrejection.ts b/packages/node/src/integrations/onunhandledrejection.ts index 9f3801b7a0cf..e1bc0b4145cf 100644 --- a/packages/node/src/integrations/onunhandledrejection.ts +++ b/packages/node/src/integrations/onunhandledrejection.ts @@ -1,8 +1,7 @@ -import { captureException, convertIntegrationFnToClass, defineIntegration, getClient } from '@sentry/core'; -import type { Client, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; +import { captureException, defineIntegration, getClient } from '@sentry/core'; +import type { Client, IntegrationFn } from '@sentry/types'; import { consoleSandbox } from '@sentry/utils'; - -import { logAndExitProcess } from './utils/errorhandling'; +import { logAndExitProcess } from '../utils/errorhandling'; type UnhandledRejectionMode = 'none' | 'warn' | 'strict'; @@ -27,22 +26,10 @@ const _onUnhandledRejectionIntegration = ((options: Partial void }> & { - new (options?: Partial<{ mode: UnhandledRejectionMode }>): Integration; -}; - -// eslint-disable-next-line deprecation/deprecation -export type OnUnhandledRejection = typeof OnUnhandledRejection; +export const onUnhandledRejectionIntegration = defineIntegration(_onUnhandledRejectionIntegration); /** * Send an exception with reason diff --git a/packages/node/src/integrations/spotlight.ts b/packages/node/src/integrations/spotlight.ts index eb9c34260b61..21629ad340ac 100644 --- a/packages/node/src/integrations/spotlight.ts +++ b/packages/node/src/integrations/spotlight.ts @@ -1,7 +1,6 @@ import * as http from 'http'; -import { URL } from 'url'; -import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; -import type { Client, Envelope, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; +import { defineIntegration } from '@sentry/core'; +import type { Client, Envelope, IntegrationFn } from '@sentry/types'; import { logger, serializeEnvelope } from '@sentry/utils'; type SpotlightConnectionOptions = { @@ -30,30 +29,14 @@ const _spotlightIntegration = ((options: Partial = { }; }) satisfies IntegrationFn; -export const spotlightIntegration = defineIntegration(_spotlightIntegration); - /** * Use this integration to send errors and transactions to Spotlight. * * Learn more about spotlight at https://spotlightjs.com * * Important: This integration only works with Node 18 or newer. - * - * @deprecated Use `spotlightIntegration()` instead. */ -// eslint-disable-next-line deprecation/deprecation -export const Spotlight = convertIntegrationFnToClass(INTEGRATION_NAME, spotlightIntegration) as IntegrationClass< - Integration & { setup: (client: Client) => void } -> & { - new ( - options?: Partial<{ - sidecarUrl?: string; - }>, - ): Integration; -}; - -// eslint-disable-next-line deprecation/deprecation -export type Spotlight = typeof Spotlight; +export const spotlightIntegration = defineIntegration(_spotlightIntegration); function connectToSpotlight(client: Client, options: Required): void { const spotlightUrl = parseSidecarUrl(options.sidecarUrl); diff --git a/packages/node-experimental/src/integrations/tracing/express.ts b/packages/node/src/integrations/tracing/express.ts similarity index 100% rename from packages/node-experimental/src/integrations/tracing/express.ts rename to packages/node/src/integrations/tracing/express.ts diff --git a/packages/node-experimental/src/integrations/tracing/fastify.ts b/packages/node/src/integrations/tracing/fastify.ts similarity index 100% rename from packages/node-experimental/src/integrations/tracing/fastify.ts rename to packages/node/src/integrations/tracing/fastify.ts diff --git a/packages/node-experimental/src/integrations/tracing/graphql.ts b/packages/node/src/integrations/tracing/graphql.ts similarity index 100% rename from packages/node-experimental/src/integrations/tracing/graphql.ts rename to packages/node/src/integrations/tracing/graphql.ts diff --git a/packages/node-experimental/src/integrations/tracing/hapi/index.ts b/packages/node/src/integrations/tracing/hapi/index.ts similarity index 100% rename from packages/node-experimental/src/integrations/tracing/hapi/index.ts rename to packages/node/src/integrations/tracing/hapi/index.ts diff --git a/packages/node-experimental/src/integrations/tracing/hapi/types.ts b/packages/node/src/integrations/tracing/hapi/types.ts similarity index 100% rename from packages/node-experimental/src/integrations/tracing/hapi/types.ts rename to packages/node/src/integrations/tracing/hapi/types.ts diff --git a/packages/node-experimental/src/integrations/tracing/index.ts b/packages/node/src/integrations/tracing/index.ts similarity index 100% rename from packages/node-experimental/src/integrations/tracing/index.ts rename to packages/node/src/integrations/tracing/index.ts diff --git a/packages/node-experimental/src/integrations/tracing/koa.ts b/packages/node/src/integrations/tracing/koa.ts similarity index 60% rename from packages/node-experimental/src/integrations/tracing/koa.ts rename to packages/node/src/integrations/tracing/koa.ts index 2d85703c054a..2c3a57ebed3f 100644 --- a/packages/node-experimental/src/integrations/tracing/koa.ts +++ b/packages/node/src/integrations/tracing/koa.ts @@ -1,6 +1,6 @@ import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { KoaInstrumentation } from '@opentelemetry/instrumentation-koa'; -import { defineIntegration } from '@sentry/core'; +import { captureException, defineIntegration } from '@sentry/core'; import type { IntegrationFn } from '@sentry/types'; const _koaIntegration = (() => { @@ -15,3 +15,13 @@ const _koaIntegration = (() => { }) satisfies IntegrationFn; export const koaIntegration = defineIntegration(_koaIntegration); + +export const setupKoaErrorHandler = (app: { use: (arg0: (ctx: any, next: any) => Promise) => void }): void => { + app.use(async (ctx, next) => { + try { + await next(); + } catch (error) { + captureException(error); + } + }); +}; diff --git a/packages/node-experimental/src/integrations/tracing/mongo.ts b/packages/node/src/integrations/tracing/mongo.ts similarity index 100% rename from packages/node-experimental/src/integrations/tracing/mongo.ts rename to packages/node/src/integrations/tracing/mongo.ts diff --git a/packages/node-experimental/src/integrations/tracing/mongoose.ts b/packages/node/src/integrations/tracing/mongoose.ts similarity index 100% rename from packages/node-experimental/src/integrations/tracing/mongoose.ts rename to packages/node/src/integrations/tracing/mongoose.ts diff --git a/packages/node-experimental/src/integrations/tracing/mysql.ts b/packages/node/src/integrations/tracing/mysql.ts similarity index 100% rename from packages/node-experimental/src/integrations/tracing/mysql.ts rename to packages/node/src/integrations/tracing/mysql.ts diff --git a/packages/node-experimental/src/integrations/tracing/mysql2.ts b/packages/node/src/integrations/tracing/mysql2.ts similarity index 100% rename from packages/node-experimental/src/integrations/tracing/mysql2.ts rename to packages/node/src/integrations/tracing/mysql2.ts diff --git a/packages/node-experimental/src/integrations/tracing/nest.ts b/packages/node/src/integrations/tracing/nest.ts similarity index 58% rename from packages/node-experimental/src/integrations/tracing/nest.ts rename to packages/node/src/integrations/tracing/nest.ts index 1f2c75e3807e..fb9a211b2a1e 100644 --- a/packages/node-experimental/src/integrations/tracing/nest.ts +++ b/packages/node/src/integrations/tracing/nest.ts @@ -1,6 +1,6 @@ import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core'; -import { defineIntegration } from '@sentry/core'; +import { captureException, defineIntegration } from '@sentry/core'; import type { IntegrationFn } from '@sentry/types'; const _nestIntegration = (() => { @@ -20,3 +20,18 @@ const _nestIntegration = (() => { * Capture tracing data for nest. */ export const nestIntegration = defineIntegration(_nestIntegration); + +const SentryNestExceptionFilter = { + catch(exception: unknown) { + captureException(exception); + }, +}; + +/** + * Setup an error handler for Nest. + */ +export function setupNestErrorHandler(app: { + useGlobalFilters: (arg0: { catch(exception: unknown): void }) => void; +}): void { + app.useGlobalFilters(SentryNestExceptionFilter); +} diff --git a/packages/node-experimental/src/integrations/tracing/postgres.ts b/packages/node/src/integrations/tracing/postgres.ts similarity index 100% rename from packages/node-experimental/src/integrations/tracing/postgres.ts rename to packages/node/src/integrations/tracing/postgres.ts diff --git a/packages/node-experimental/src/integrations/tracing/prisma.ts b/packages/node/src/integrations/tracing/prisma.ts similarity index 100% rename from packages/node-experimental/src/integrations/tracing/prisma.ts rename to packages/node/src/integrations/tracing/prisma.ts diff --git a/packages/node/src/integrations/undici/index.ts b/packages/node/src/integrations/undici/index.ts deleted file mode 100644 index 2a0ea01fe234..000000000000 --- a/packages/node/src/integrations/undici/index.ts +++ /dev/null @@ -1,344 +0,0 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; -import { - SPAN_STATUS_ERROR, - addBreadcrumb, - defineIntegration, - getClient, - getCurrentScope, - getDynamicSamplingContextFromClient, - getDynamicSamplingContextFromSpan, - getIsolationScope, - hasTracingEnabled, - isSentryRequestUrl, - setHttpStatus, - spanToTraceHeader, -} from '@sentry/core'; -import type { Integration, IntegrationFn, Span, SpanAttributes } from '@sentry/types'; -import { - LRUMap, - dynamicSamplingContextToSentryBaggageHeader, - generateSentryTraceHeader, - getSanitizedUrlString, - parseUrl, - stringMatchesSomePattern, -} from '@sentry/utils'; - -import type { NodeClient } from '../../client'; -import { NODE_VERSION } from '../../nodeVersion'; -import type { - DiagnosticsChannel, - RequestCreateMessage, - RequestEndMessage, - RequestErrorMessage, - RequestWithSentry, -} from './types'; - -export enum ChannelName { - // https://github.com/nodejs/undici/blob/e6fc80f809d1217814c044f52ed40ef13f21e43c/docs/api/DiagnosticsChannel.md#undicirequestcreate - RequestCreate = 'undici:request:create', - RequestEnd = 'undici:request:headers', - RequestError = 'undici:request:error', -} - -export interface UndiciOptions { - /** - * Whether breadcrumbs should be recorded for requests - * Defaults to true - */ - breadcrumbs: boolean; - - /** - * Whether tracing spans should be created for requests - * If not set, this will be enabled/disabled based on if tracing is enabled. - */ - tracing?: boolean; - - /** - * Function determining whether or not to create spans to track outgoing requests to the given URL. - * By default, spans will be created for all outgoing requests. - */ - shouldCreateSpanForRequest?: (url: string) => boolean; -} - -// Please note that you cannot use `console.log` to debug the callbacks registered to the `diagnostics_channel` API. -// To debug, you can use `writeFileSync` to write to a file: -// https://nodejs.org/api/async_hooks.html#printing-in-asynchook-callbacks -// -// import { writeFileSync } from 'fs'; -// import { format } from 'util'; -// -// function debug(...args: any): void { -// // Use a function like this one when debugging inside an AsyncHook callback -// // @ts-expect-error any -// writeFileSync('log.out', `${format(...args)}\n`, { flag: 'a' }); -// } - -const _nativeNodeFetchintegration = ((options?: Partial) => { - // eslint-disable-next-line deprecation/deprecation - return new Undici(options) as unknown as Integration; -}) satisfies IntegrationFn; - -export const nativeNodeFetchintegration = defineIntegration(_nativeNodeFetchintegration); - -/** - * Instruments outgoing HTTP requests made with the `undici` package via - * Node's `diagnostics_channel` API. - * - * Supports Undici 4.7.0 or higher. - * - * Requires Node 16.17.0 or higher. - * - * @deprecated Use `nativeNodeFetchintegration()` instead. - */ -export class Undici implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'Undici'; - - /** - * @inheritDoc - */ - // eslint-disable-next-line deprecation/deprecation - public name: string = Undici.id; - - private readonly _options: UndiciOptions; - - private readonly _createSpanUrlMap: LRUMap = new LRUMap(100); - private readonly _headersUrlMap: LRUMap = new LRUMap(100); - - public constructor(_options: Partial = {}) { - this._options = { - breadcrumbs: _options.breadcrumbs === undefined ? true : _options.breadcrumbs, - tracing: _options.tracing, - shouldCreateSpanForRequest: _options.shouldCreateSpanForRequest, - }; - } - - /** - * @inheritDoc - */ - public setupOnce(): void { - // Requires Node 16+ to use the diagnostics_channel API. - if (NODE_VERSION.major < 16) { - return; - } - - let ds: DiagnosticsChannel | undefined; - try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - ds = require('diagnostics_channel') as DiagnosticsChannel; - } catch (e) { - // no-op - } - - if (!ds || !ds.subscribe) { - return; - } - - // https://github.com/nodejs/undici/blob/e6fc80f809d1217814c044f52ed40ef13f21e43c/docs/api/DiagnosticsChannel.md - ds.subscribe(ChannelName.RequestCreate, this._onRequestCreate); - ds.subscribe(ChannelName.RequestEnd, this._onRequestEnd); - ds.subscribe(ChannelName.RequestError, this._onRequestError); - } - - /** Helper that wraps shouldCreateSpanForRequest option */ - private _shouldCreateSpan(url: string): boolean { - if (this._options.tracing === false || (this._options.tracing === undefined && !hasTracingEnabled())) { - return false; - } - - if (this._options.shouldCreateSpanForRequest === undefined) { - return true; - } - - const cachedDecision = this._createSpanUrlMap.get(url); - if (cachedDecision !== undefined) { - return cachedDecision; - } - - const decision = this._options.shouldCreateSpanForRequest(url); - this._createSpanUrlMap.set(url, decision); - return decision; - } - - private _onRequestCreate = (message: unknown): void => { - if (!getClient()?.getIntegrationByName('Undici')) { - return; - } - - const { request } = message as RequestCreateMessage; - - const stringUrl = request.origin ? request.origin.toString() + request.path : request.path; - - const client = getClient(); - if (!client) { - return; - } - - if (isSentryRequestUrl(stringUrl, client) || request.__sentry_span__ !== undefined) { - return; - } - - const clientOptions = client.getOptions(); - const scope = getCurrentScope(); - const isolationScope = getIsolationScope(); - - const span = this._shouldCreateSpan(stringUrl) ? createRequestSpan(request, stringUrl) : undefined; - if (span) { - request.__sentry_span__ = span; - } - - const shouldAttachTraceData = (url: string): boolean => { - if (clientOptions.tracePropagationTargets === undefined) { - return true; - } - - const cachedDecision = this._headersUrlMap.get(url); - if (cachedDecision !== undefined) { - return cachedDecision; - } - - const decision = stringMatchesSomePattern(url, clientOptions.tracePropagationTargets); - this._headersUrlMap.set(url, decision); - return decision; - }; - - if (shouldAttachTraceData(stringUrl)) { - const { traceId, spanId, sampled, dsc } = { - ...isolationScope.getPropagationContext(), - ...scope.getPropagationContext(), - }; - - const sentryTraceHeader = span ? spanToTraceHeader(span) : generateSentryTraceHeader(traceId, spanId, sampled); - - const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader( - dsc || (span ? getDynamicSamplingContextFromSpan(span) : getDynamicSamplingContextFromClient(traceId, client)), - ); - - setHeadersOnRequest(request, sentryTraceHeader, sentryBaggageHeader); - } - }; - - private _onRequestEnd = (message: unknown): void => { - if (!getClient()?.getIntegrationByName('Undici')) { - return; - } - - const { request, response } = message as RequestEndMessage; - - const stringUrl = request.origin ? request.origin.toString() + request.path : request.path; - - if (isSentryRequestUrl(stringUrl, getClient())) { - return; - } - - const span = request.__sentry_span__; - if (span) { - setHttpStatus(span, response.statusCode); - span.end(); - } - - if (this._options.breadcrumbs) { - addBreadcrumb( - { - category: 'http', - data: { - method: request.method, - status_code: response.statusCode, - url: stringUrl, - }, - type: 'http', - }, - { - event: 'response', - request, - response, - }, - ); - } - }; - - private _onRequestError = (message: unknown): void => { - if (!getClient()?.getIntegrationByName('Undici')) { - return; - } - - const { request } = message as RequestErrorMessage; - - const stringUrl = request.origin ? request.origin.toString() + request.path : request.path; - - if (isSentryRequestUrl(stringUrl, getClient())) { - return; - } - - const span = request.__sentry_span__; - if (span) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); - span.end(); - } - - if (this._options.breadcrumbs) { - addBreadcrumb( - { - category: 'http', - data: { - method: request.method, - url: stringUrl, - }, - level: 'error', - type: 'http', - }, - { - event: 'error', - request, - }, - ); - } - }; -} - -function setHeadersOnRequest( - request: RequestWithSentry, - sentryTrace: string, - sentryBaggageHeader: string | undefined, -): void { - let hasSentryHeaders: boolean; - if (Array.isArray(request.headers)) { - hasSentryHeaders = request.headers.some(headerLine => headerLine === 'sentry-trace'); - } else { - const headerLines = request.headers.split('\r\n'); - hasSentryHeaders = headerLines.some(headerLine => headerLine.startsWith('sentry-trace:')); - } - - if (hasSentryHeaders) { - return; - } - - request.addHeader('sentry-trace', sentryTrace); - if (sentryBaggageHeader) { - request.addHeader('baggage', sentryBaggageHeader); - } -} - -function createRequestSpan(request: RequestWithSentry, stringUrl: string): Span { - const url = parseUrl(stringUrl); - - const method = request.method || 'GET'; - const attributes: SpanAttributes = { - 'http.method': method, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.node.undici', - }; - if (url.search) { - attributes['http.query'] = url.search; - } - if (url.hash) { - attributes['http.fragment'] = url.hash; - } - return startInactiveSpan({ - onlyIfParent: true, - op: 'http.client', - name: `${method} ${getSanitizedUrlString(url)}`, - attributes, - }); -} diff --git a/packages/node/src/integrations/undici/types.ts b/packages/node/src/integrations/undici/types.ts deleted file mode 100644 index 05732811dc68..000000000000 --- a/packages/node/src/integrations/undici/types.ts +++ /dev/null @@ -1,255 +0,0 @@ -// Vendored from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/5a94716c6788f654aea7999a5fc28f4f1e7c48ad/types/node/diagnostics_channel.d.ts - -import type { URL } from 'url'; -import type { Span } from '@sentry/types'; - -// License: -// This project is licensed under the MIT license. -// Copyrights are respective of each contributor listed at the beginning of each definition file. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -// documentation files(the "Software"), to deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and / or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS -// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -// Vendored code starts here: - -export type ChannelListener = (message: unknown, name: string | symbol) => void; - -/** - * The `diagnostics_channel` module provides an API to create named channels - * to report arbitrary message data for diagnostics purposes. - * - * It can be accessed using: - * - * ```js - * import diagnostics_channel from 'diagnostics_channel'; - * ``` - * - * It is intended that a module writer wanting to report diagnostics messages - * will create one or many top-level channels to report messages through. - * Channels may also be acquired at runtime but it is not encouraged - * due to the additional overhead of doing so. Channels may be exported for - * convenience, but as long as the name is known it can be acquired anywhere. - * - * If you intend for your module to produce diagnostics data for others to - * consume it is recommended that you include documentation of what named - * channels are used along with the shape of the message data. Channel names - * should generally include the module name to avoid collisions with data from - * other modules. - * @experimental - * @see [source](https://github.com/nodejs/node/blob/v18.0.0/lib/diagnostics_channel.js) - */ -export interface DiagnosticsChannel { - /** - * Check if there are active subscribers to the named channel. This is helpful if - * the message you want to send might be expensive to prepare. - * - * This API is optional but helpful when trying to publish messages from very - * performance-sensitive code. - * - * ```js - * import diagnostics_channel from 'diagnostics_channel'; - * - * if (diagnostics_channel.hasSubscribers('my-channel')) { - * // There are subscribers, prepare and publish message - * } - * ``` - * @since v15.1.0, v14.17.0 - * @param name The channel name - * @return If there are active subscribers - */ - hasSubscribers(name: string | symbol): boolean; - /** - * This is the primary entry-point for anyone wanting to interact with a named - * channel. It produces a channel object which is optimized to reduce overhead at - * publish time as much as possible. - * - * ```js - * import diagnostics_channel from 'diagnostics_channel'; - * - * const channel = diagnostics_channel.channel('my-channel'); - * ``` - * @since v15.1.0, v14.17.0 - * @param name The channel name - * @return The named channel object - */ - channel(name: string | symbol): Channel; - /** - * Register a message handler to subscribe to this channel. This message handler will be run synchronously - * whenever a message is published to the channel. Any errors thrown in the message handler will - * trigger an 'uncaughtException'. - * - * ```js - * import diagnostics_channel from 'diagnostics_channel'; - * - * diagnostics_channel.subscribe('my-channel', (message, name) => { - * // Received data - * }); - * ``` - * - * @since v18.7.0, v16.17.0 - * @param name The channel name - * @param onMessage The handler to receive channel messages - */ - subscribe(name: string | symbol, onMessage: ChannelListener): void; - /** - * Remove a message handler previously registered to this channel with diagnostics_channel.subscribe(name, onMessage). - * - * ```js - * import diagnostics_channel from 'diagnostics_channel'; - * - * function onMessage(message, name) { - * // Received data - * } - * - * diagnostics_channel.subscribe('my-channel', onMessage); - * - * diagnostics_channel.unsubscribe('my-channel', onMessage); - * ``` - * - * @since v18.7.0, v16.17.0 - * @param name The channel name - * @param onMessage The previous subscribed handler to remove - * @returns `true` if the handler was found, `false` otherwise - */ - unsubscribe(name: string | symbol, onMessage: ChannelListener): boolean; -} - -/** - * The class `Channel` represents an individual named channel within the data - * pipeline. It is use to track subscribers and to publish messages when there - * are subscribers present. It exists as a separate object to avoid channel - * lookups at publish time, enabling very fast publish speeds and allowing - * for heavy use while incurring very minimal cost. Channels are created with {@link channel}, constructing a channel directly - * with `new Channel(name)` is not supported. - * @since v15.1.0, v14.17.0 - */ -interface ChannelI { - readonly name: string | symbol; - /** - * Check if there are active subscribers to this channel. This is helpful if - * the message you want to send might be expensive to prepare. - * - * This API is optional but helpful when trying to publish messages from very - * performance-sensitive code. - * - * ```js - * import diagnostics_channel from 'diagnostics_channel'; - * - * const channel = diagnostics_channel.channel('my-channel'); - * - * if (channel.hasSubscribers) { - * // There are subscribers, prepare and publish message - * } - * ``` - * @since v15.1.0, v14.17.0 - */ - readonly hasSubscribers: boolean; - - /** - * Publish a message to any subscribers to the channel. This will - * trigger message handlers synchronously so they will execute within - * the same context. - * - * ```js - * import diagnostics_channel from 'diagnostics_channel'; - * - * const channel = diagnostics_channel.channel('my-channel'); - * - * channel.publish({ - * some: 'message' - * }); - * ``` - * @since v15.1.0, v14.17.0 - * @param message The message to send to the channel subscribers - */ - publish(message: unknown): void; - /** - * Register a message handler to subscribe to this channel. This message handler - * will be run synchronously whenever a message is published to the channel. Any - * errors thrown in the message handler will trigger an `'uncaughtException'`. - * - * ```js - * import diagnostics_channel from 'diagnostics_channel'; - * - * const channel = diagnostics_channel.channel('my-channel'); - * - * channel.subscribe((message, name) => { - * // Received data - * }); - * ``` - * @since v15.1.0, v14.17.0 - * @param onMessage The handler to receive channel messages - */ - subscribe(onMessage: ChannelListener): void; - /** - * Remove a message handler previously registered to this channel with `channel.subscribe(onMessage)`. - * - * ```js - * import diagnostics_channel from 'diagnostics_channel'; - * - * const channel = diagnostics_channel.channel('my-channel'); - * - * function onMessage(message, name) { - * // Received data - * } - * - * channel.subscribe(onMessage); - * - * channel.unsubscribe(onMessage); - * ``` - * @since v15.1.0, v14.17.0 - * @param onMessage The previous subscribed handler to remove - * @return `true` if the handler was found, `false` otherwise. - */ - unsubscribe(onMessage: ChannelListener): void; -} - -export interface Channel extends ChannelI { - new (name: string | symbol): void; -} - -// https://github.com/nodejs/undici/blob/e6fc80f809d1217814c044f52ed40ef13f21e43c/types/diagnostics-channel.d.ts -export interface UndiciRequest { - origin?: string | URL; - completed: boolean; - // Originally was Dispatcher.HttpMethod, but did not want to vendor that in. - method?: string; - path: string; - // string for undici@<=6.6.2 and string[] for undici@>=6.7.0. - // see for more information: https://github.com/getsentry/sentry-javascript/issues/10936 - headers: string | string[]; - addHeader(key: string, value: string): RequestWithSentry; -} - -export interface UndiciResponse { - statusCode: number; - statusText: string; - headers: Array; -} - -export interface RequestWithSentry extends UndiciRequest { - __sentry_span__?: Span; -} - -export interface RequestCreateMessage { - request: RequestWithSentry; -} - -export interface RequestEndMessage { - request: RequestWithSentry; - response: UndiciResponse; -} - -export interface RequestErrorMessage { - request: RequestWithSentry; - error: Error; -} diff --git a/packages/node/src/integrations/utils/errorhandling.ts b/packages/node/src/integrations/utils/errorhandling.ts deleted file mode 100644 index fc2a2c2b8e7b..000000000000 --- a/packages/node/src/integrations/utils/errorhandling.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { getClient } from '@sentry/core'; -import { consoleSandbox, logger } from '@sentry/utils'; - -import type { NodeClient } from '../../client'; -import { DEBUG_BUILD } from '../../debug-build'; - -const DEFAULT_SHUTDOWN_TIMEOUT = 2000; - -/** - * @hidden - */ -export function logAndExitProcess(error: Error): void { - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.error(error); - }); - - const client = getClient(); - - if (client === undefined) { - DEBUG_BUILD && logger.warn('No NodeClient was defined, we are exiting the process now.'); - global.process.exit(1); - return; - } - - const options = client.getOptions(); - const timeout = - (options && options.shutdownTimeout && options.shutdownTimeout > 0 && options.shutdownTimeout) || - DEFAULT_SHUTDOWN_TIMEOUT; - client.close(timeout).then( - (result: boolean) => { - if (!result) { - DEBUG_BUILD && logger.warn('We reached the timeout for emptying the request buffer, still exiting now!'); - } - global.process.exit(1); - }, - error => { - DEBUG_BUILD && logger.error(error); - }, - ); -} diff --git a/packages/node/src/integrations/utils/http.ts b/packages/node/src/integrations/utils/http.ts deleted file mode 100644 index 82319c2fcdb8..000000000000 --- a/packages/node/src/integrations/utils/http.ts +++ /dev/null @@ -1,223 +0,0 @@ -import type * as http from 'node:http'; -import type * as https from 'node:https'; -import { URL } from 'url'; - -import { NODE_VERSION } from '../../nodeVersion'; - -/** - * Assembles a URL that's passed to the users to filter on. - * It can include raw (potentially PII containing) data, which we'll allow users to access to filter - * but won't include in spans or breadcrumbs. - * - * @param requestOptions RequestOptions object containing the component parts for a URL - * @returns Fully-formed URL - */ -// TODO (v8): This function should include auth, query and fragment (it's breaking, so we need to wait for v8) -export function extractRawUrl(requestOptions: RequestOptions): string { - const { protocol, hostname, port } = parseRequestOptions(requestOptions); - const path = requestOptions.path ? requestOptions.path : '/'; - return `${protocol}//${hostname}${port}${path}`; -} - -/** - * Assemble a URL to be used for breadcrumbs and spans. - * - * @param requestOptions RequestOptions object containing the component parts for a URL - * @returns Fully-formed URL - */ -export function extractUrl(requestOptions: RequestOptions): string { - const { protocol, hostname, port } = parseRequestOptions(requestOptions); - - const path = requestOptions.pathname || '/'; - - // always filter authority, see https://develop.sentry.dev/sdk/data-handling/#structuring-data - const authority = requestOptions.auth ? redactAuthority(requestOptions.auth) : ''; - - return `${protocol}//${authority}${hostname}${port}${path}`; -} - -function redactAuthority(auth: string): string { - const [user, password] = auth.split(':'); - return `${user ? '[Filtered]' : ''}:${password ? '[Filtered]' : ''}@`; -} - -/** - * Handle various edge cases in the span name (for spans representing http(s) requests). - * - * @param description current `name` property of the span representing the request - * @param requestOptions Configuration data for the request - * @param Request Request object - * - * @returns The cleaned name - */ -export function cleanSpanName( - name: string | undefined, - requestOptions: RequestOptions, - request: http.ClientRequest, -): string | undefined { - // nothing to clean - if (!name) { - return name; - } - - // eslint-disable-next-line prefer-const - let [method, requestUrl] = name.split(' '); - - // superagent sticks the protocol in a weird place (we check for host because if both host *and* protocol are missing, - // we're likely dealing with an internal route and this doesn't apply) - if (requestOptions.host && !requestOptions.protocol) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any - requestOptions.protocol = (request as any)?.agent?.protocol; // worst comes to worst, this is undefined and nothing changes - // This URL contains the filtered authority ([filtered]:[filtered]@example.com) but no fragment or query params - requestUrl = extractUrl(requestOptions); - } - - // internal routes can end up starting with a triple slash rather than a single one - if (requestUrl?.startsWith('///')) { - requestUrl = requestUrl.slice(2); - } - - return `${method} ${requestUrl}`; -} - -// the node types are missing a few properties which node's `urlToOptions` function spits out -export type RequestOptions = http.RequestOptions & { hash?: string; search?: string; pathname?: string; href?: string }; -type RequestCallback = (response: http.IncomingMessage) => void; -export type RequestMethodArgs = - | [RequestOptions | string | URL, RequestCallback?] - | [string | URL, RequestOptions, RequestCallback?]; -export type RequestMethod = (...args: RequestMethodArgs) => http.ClientRequest; - -/** - * Convert a URL object into a RequestOptions object. - * - * Copied from Node's internals (where it's used in http(s).request() and http(s).get()), modified only to use the - * RequestOptions type above. - * - * See https://github.com/nodejs/node/blob/master/lib/internal/url.js. - */ -export function urlToOptions(url: URL): RequestOptions { - const options: RequestOptions = { - protocol: url.protocol, - hostname: - typeof url.hostname === 'string' && url.hostname.startsWith('[') ? url.hostname.slice(1, -1) : url.hostname, - hash: url.hash, - search: url.search, - pathname: url.pathname, - path: `${url.pathname || ''}${url.search || ''}`, - href: url.href, - }; - if (url.port !== '') { - options.port = Number(url.port); - } - if (url.username || url.password) { - options.auth = `${url.username}:${url.password}`; - } - return options; -} - -/** - * Normalize inputs to `http(s).request()` and `http(s).get()`. - * - * Legal inputs to `http(s).request()` and `http(s).get()` can take one of ten forms: - * [ RequestOptions | string | URL ], - * [ RequestOptions | string | URL, RequestCallback ], - * [ string | URL, RequestOptions ], and - * [ string | URL, RequestOptions, RequestCallback ]. - * - * This standardizes to one of two forms: [ RequestOptions ] and [ RequestOptions, RequestCallback ]. A similar thing is - * done as the first step of `http(s).request()` and `http(s).get()`; this just does it early so that we can interact - * with the args in a standard way. - * - * @param requestArgs The inputs to `http(s).request()` or `http(s).get()`, as an array. - * - * @returns Equivalent args of the form [ RequestOptions ] or [ RequestOptions, RequestCallback ]. - */ -export function normalizeRequestArgs( - httpModule: typeof http | typeof https, - requestArgs: RequestMethodArgs, -): [RequestOptions] | [RequestOptions, RequestCallback] { - let callback, requestOptions; - - // pop off the callback, if there is one - if (typeof requestArgs[requestArgs.length - 1] === 'function') { - callback = requestArgs.pop() as RequestCallback; - } - - // create a RequestOptions object of whatever's at index 0 - if (typeof requestArgs[0] === 'string') { - requestOptions = urlToOptions(new URL(requestArgs[0])); - } else if (requestArgs[0] instanceof URL) { - requestOptions = urlToOptions(requestArgs[0]); - } else { - requestOptions = requestArgs[0]; - - try { - const parsed = new URL( - requestOptions.path || '', - `${requestOptions.protocol || 'http:'}//${requestOptions.hostname}`, - ); - requestOptions = { - pathname: parsed.pathname, - search: parsed.search, - hash: parsed.hash, - ...requestOptions, - }; - } catch (e) { - // ignore - } - } - - // if the options were given separately from the URL, fold them in - if (requestArgs.length === 2) { - requestOptions = { ...requestOptions, ...requestArgs[1] }; - } - - // Figure out the protocol if it's currently missing - if (requestOptions.protocol === undefined) { - // Worst case we end up populating protocol with undefined, which it already is - /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any */ - - // NOTE: Prior to Node 9, `https` used internals of `http` module, thus we don't patch it. - // Because of that, we cannot rely on `httpModule` to provide us with valid protocol, - // as it will always return `http`, even when using `https` module. - // - // See test/integrations/http.test.ts for more details on Node <=v8 protocol issue. - if (NODE_VERSION.major > 8) { - requestOptions.protocol = - (httpModule?.globalAgent as any)?.protocol || - (requestOptions.agent as any)?.protocol || - (requestOptions._defaultAgent as any)?.protocol; - } else { - requestOptions.protocol = - (requestOptions.agent as any)?.protocol || - (requestOptions._defaultAgent as any)?.protocol || - (httpModule?.globalAgent as any)?.protocol; - } - /* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any */ - } - - // return args in standardized form - if (callback) { - return [requestOptions, callback]; - } else { - return [requestOptions]; - } -} - -function parseRequestOptions(requestOptions: RequestOptions): { - protocol: string; - hostname: string; - port: string; -} { - const protocol = requestOptions.protocol || ''; - const hostname = requestOptions.hostname || requestOptions.host || ''; - // Don't log standard :80 (http) and :443 (https) ports to reduce the noise - // Also don't add port if the hostname already includes a port - const port = - !requestOptions.port || requestOptions.port === 80 || requestOptions.port === 443 || /^(.*):(\d+)$/.test(hostname) - ? '' - : `:${requestOptions.port}`; - - return { protocol, hostname, port }; -} diff --git a/packages/node/src/module.ts b/packages/node/src/module.ts deleted file mode 100644 index d873bf9b2f2e..000000000000 --- a/packages/node/src/module.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { posix, sep } from 'path'; -import { dirname } from '@sentry/utils'; - -/** normalizes Windows paths */ -function normalizeWindowsPath(path: string): string { - return path - .replace(/^[A-Z]:/, '') // remove Windows-style prefix - .replace(/\\/g, '/'); // replace all `\` instances with `/` -} - -/** Creates a function that gets the module name from a filename */ -export function createGetModuleFromFilename( - basePath: string = process.argv[1] ? dirname(process.argv[1]) : process.cwd(), - isWindows: boolean = sep === '\\', -): (filename: string | undefined) => string | undefined { - const normalizedBase = isWindows ? normalizeWindowsPath(basePath) : basePath; - - return (filename: string | undefined) => { - if (!filename) { - return; - } - - const normalizedFilename = isWindows ? normalizeWindowsPath(filename) : filename; - - // eslint-disable-next-line prefer-const - let { dir, base: file, ext } = posix.parse(normalizedFilename); - - if (ext === '.js' || ext === '.mjs' || ext === '.cjs') { - file = file.slice(0, ext.length * -1); - } - - if (!dir) { - // No dirname whatsoever - dir = '.'; - } - - const n = dir.lastIndexOf('/node_modules'); - if (n > -1) { - return `${dir.slice(n + 14).replace(/\//g, '.')}:${file}`; - } - - // Let's see if it's a part of the main module - // To be a part of main module, it has to share the same base - if (dir.startsWith(normalizedBase)) { - let moduleName = dir.slice(normalizedBase.length + 1).replace(/\//g, '.'); - - if (moduleName) { - moduleName += ':'; - } - moduleName += file; - - return moduleName; - } - - return file; - }; -} diff --git a/packages/node/src/nodeVersion.ts b/packages/node/src/nodeVersion.ts index 1574237f3fb4..1f07883b771b 100644 --- a/packages/node/src/nodeVersion.ts +++ b/packages/node/src/nodeVersion.ts @@ -1,3 +1,4 @@ import { parseSemver } from '@sentry/utils'; export const NODE_VERSION = parseSemver(process.versions.node) as { major: number; minor: number; patch: number }; +export const NODE_MAJOR = NODE_VERSION.major; diff --git a/packages/node-experimental/src/otel/contextManager.ts b/packages/node/src/otel/contextManager.ts similarity index 100% rename from packages/node-experimental/src/otel/contextManager.ts rename to packages/node/src/otel/contextManager.ts diff --git a/packages/node/src/proxy/helpers.ts b/packages/node/src/proxy/helpers.ts index a5064408855d..031878511f6c 100644 --- a/packages/node/src/proxy/helpers.ts +++ b/packages/node/src/proxy/helpers.ts @@ -30,8 +30,6 @@ import * as http from 'node:http'; import * as https from 'node:https'; import type { Readable } from 'stream'; -// TODO (v8): Remove this when Node < 12 is no longer supported -import type { URL } from 'url'; export type ThenableRequest = http.ClientRequest & { then: Promise['then']; diff --git a/packages/node/src/proxy/index.ts b/packages/node/src/proxy/index.ts index 15c700ed3e62..83f72d56fb4e 100644 --- a/packages/node/src/proxy/index.ts +++ b/packages/node/src/proxy/index.ts @@ -32,8 +32,6 @@ import type * as http from 'http'; import type { OutgoingHttpHeaders } from 'http'; import * as net from 'net'; import * as tls from 'tls'; -// TODO (v8): Remove this when Node < 12 is no longer supported -import { URL } from 'url'; import { logger } from '@sentry/utils'; import { Agent } from './base'; import type { AgentConnectOpts } from './base'; diff --git a/packages/node/src/sdk.ts b/packages/node/src/sdk.ts deleted file mode 100644 index 1225cce83485..000000000000 --- a/packages/node/src/sdk.ts +++ /dev/null @@ -1,269 +0,0 @@ -import { - endSession, - functionToStringIntegration, - getClient, - getCurrentScope, - getIntegrationsToSetup, - getIsolationScope, - getMainCarrier, - inboundFiltersIntegration, - initAndBind, - linkedErrorsIntegration, - requestDataIntegration, - startSession, -} from '@sentry/core'; -import type { Integration, Options, SessionStatus, StackParser } from '@sentry/types'; -import { - GLOBAL_OBJ, - createStackParser, - nodeStackLineParser, - propagationContextFromHeaders, - stackParserFromStackParserOptions, -} from '@sentry/utils'; - -import { setNodeAsyncContextStrategy } from './async'; -import { NodeClient } from './client'; -import { consoleIntegration } from './integrations/console'; -import { nodeContextIntegration } from './integrations/context'; -import { contextLinesIntegration } from './integrations/contextlines'; -import { httpIntegration } from './integrations/http'; -import { localVariablesIntegration } from './integrations/local-variables'; -import { modulesIntegration } from './integrations/modules'; -import { onUncaughtExceptionIntegration } from './integrations/onuncaughtexception'; -import { onUnhandledRejectionIntegration } from './integrations/onunhandledrejection'; -import { spotlightIntegration } from './integrations/spotlight'; -import { nativeNodeFetchintegration } from './integrations/undici'; -import { createGetModuleFromFilename } from './module'; -import { makeNodeTransport } from './transports'; -import type { NodeClientOptions, NodeOptions } from './types'; - -/** Get the default integrations for the Node SDK. */ -export function getDefaultIntegrations(_options: Options): Integration[] { - const carrier = getMainCarrier(); - - const autoloadedIntegrations = carrier.__SENTRY__?.integrations || []; - - return [ - // Common - inboundFiltersIntegration(), - functionToStringIntegration(), - linkedErrorsIntegration(), - requestDataIntegration(), - // Native Wrappers - consoleIntegration(), - httpIntegration(), - nativeNodeFetchintegration(), - // Global Handlers - onUncaughtExceptionIntegration(), - onUnhandledRejectionIntegration(), - // Event Info - contextLinesIntegration(), - localVariablesIntegration(), - nodeContextIntegration(), - modulesIntegration(), - ...autoloadedIntegrations, - ]; -} - -/** - * The Sentry Node SDK Client. - * - * To use this SDK, call the {@link init} function as early as possible in the - * main entry module. To set context information or send manual events, use the - * provided methods. - * - * @example - * ``` - * - * const { init } = require('@sentry/node'); - * - * init({ - * dsn: '__DSN__', - * // ... - * }); - * ``` - * - * @example - * ``` - * - * const { addBreadcrumb } = require('@sentry/node'); - * addBreadcrumb({ - * message: 'My Breadcrumb', - * // ... - * }); - * ``` - * - * @example - * ``` - * - * const Sentry = require('@sentry/node'); - * Sentry.captureMessage('Hello, world!'); - * Sentry.captureException(new Error('Good bye')); - * Sentry.captureEvent({ - * message: 'Manual', - * stacktrace: [ - * // ... - * ], - * }); - * ``` - * - * @see {@link NodeOptions} for documentation on configuration options. - */ -// eslint-disable-next-line complexity -export function init(options: NodeOptions = {}): void { - setNodeAsyncContextStrategy(); - - if (options.defaultIntegrations === undefined) { - options.defaultIntegrations = getDefaultIntegrations(options); - } - - if (options.dsn === undefined && process.env.SENTRY_DSN) { - options.dsn = process.env.SENTRY_DSN; - } - - const sentryTracesSampleRate = process.env.SENTRY_TRACES_SAMPLE_RATE; - if (options.tracesSampleRate === undefined && sentryTracesSampleRate) { - const tracesSampleRate = parseFloat(sentryTracesSampleRate); - if (isFinite(tracesSampleRate)) { - options.tracesSampleRate = tracesSampleRate; - } - } - - if (options.release === undefined) { - const detectedRelease = getSentryRelease(); - if (detectedRelease !== undefined) { - options.release = detectedRelease; - } else { - // If release is not provided, then we should disable autoSessionTracking - options.autoSessionTracking = false; - } - } - - if (options.environment === undefined && process.env.SENTRY_ENVIRONMENT) { - options.environment = process.env.SENTRY_ENVIRONMENT; - } - - if (options.autoSessionTracking === undefined && options.dsn !== undefined) { - options.autoSessionTracking = true; - } - - // TODO(v7): Refactor this to reduce the logic above - const clientOptions: NodeClientOptions = { - ...options, - stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser), - integrations: getIntegrationsToSetup(options), - transport: options.transport || makeNodeTransport, - }; - - initAndBind(options.clientClass || NodeClient, clientOptions); - - if (options.autoSessionTracking) { - startSessionTracking(); - } - - updateScopeFromEnvVariables(); - - if (options.spotlight) { - const client = getClient(); - if (client) { - // force integrations to be setup even if no DSN was set - // If they have already been added before, they will be ignored anyhow - const integrations = client.getOptions().integrations; - for (const integration of integrations) { - client.addIntegration(integration); - } - client.addIntegration( - spotlightIntegration({ sidecarUrl: typeof options.spotlight === 'string' ? options.spotlight : undefined }), - ); - } - } -} - -/** - * Function that takes an instance of NodeClient and checks if autoSessionTracking option is enabled for that client - */ -export function isAutoSessionTrackingEnabled(client?: NodeClient): boolean { - if (client === undefined) { - return false; - } - const clientOptions = client && client.getOptions(); - if (clientOptions && clientOptions.autoSessionTracking !== undefined) { - return clientOptions.autoSessionTracking; - } - return false; -} - -/** - * Returns a release dynamically from environment variables. - */ -export function getSentryRelease(fallback?: string): string | undefined { - // Always read first as Sentry takes this as precedence - if (process.env.SENTRY_RELEASE) { - return process.env.SENTRY_RELEASE; - } - - // This supports the variable that sentry-webpack-plugin injects - if (GLOBAL_OBJ.SENTRY_RELEASE && GLOBAL_OBJ.SENTRY_RELEASE.id) { - return GLOBAL_OBJ.SENTRY_RELEASE.id; - } - - return ( - // GitHub Actions - https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables#default-environment-variables - process.env.GITHUB_SHA || - // Netlify - https://docs.netlify.com/configure-builds/environment-variables/#build-metadata - process.env.COMMIT_REF || - // Vercel - https://vercel.com/docs/v2/build-step#system-environment-variables - process.env.VERCEL_GIT_COMMIT_SHA || - process.env.VERCEL_GITHUB_COMMIT_SHA || - process.env.VERCEL_GITLAB_COMMIT_SHA || - process.env.VERCEL_BITBUCKET_COMMIT_SHA || - // Zeit (now known as Vercel) - process.env.ZEIT_GITHUB_COMMIT_SHA || - process.env.ZEIT_GITLAB_COMMIT_SHA || - process.env.ZEIT_BITBUCKET_COMMIT_SHA || - // Cloudflare Pages - https://developers.cloudflare.com/pages/platform/build-configuration/#environment-variables - process.env.CF_PAGES_COMMIT_SHA || - fallback - ); -} - -/** Node.js stack parser */ -export const defaultStackParser: StackParser = createStackParser(nodeStackLineParser(createGetModuleFromFilename())); - -/** - * Enable automatic Session Tracking for the node process. - */ -function startSessionTracking(): void { - startSession(); - // Emitted in the case of healthy sessions, error of `mechanism.handled: true` and unhandledrejections because - // The 'beforeExit' event is not emitted for conditions causing explicit termination, - // such as calling process.exit() or uncaught exceptions. - // Ref: https://nodejs.org/api/process.html#process_event_beforeexit - process.on('beforeExit', () => { - const session = getIsolationScope().getSession(); - const terminalStates: SessionStatus[] = ['exited', 'crashed']; - // Only call endSession, if the Session exists on Scope and SessionStatus is not a - // Terminal Status i.e. Exited or Crashed because - // "When a session is moved away from ok it must not be updated anymore." - // Ref: https://develop.sentry.dev/sdk/sessions/ - if (session && !terminalStates.includes(session.status)) { - endSession(); - } - }); -} - -/** - * Update scope and propagation context based on environmental variables. - * - * See https://github.com/getsentry/rfcs/blob/main/text/0071-continue-trace-over-process-boundaries.md - * for more details. - */ -function updateScopeFromEnvVariables(): void { - const sentryUseEnvironment = (process.env.SENTRY_USE_ENVIRONMENT || '').toLowerCase(); - if (!['false', 'n', 'no', 'off', '0'].includes(sentryUseEnvironment)) { - const sentryTraceEnv = process.env.SENTRY_TRACE; - const baggageEnv = process.env.SENTRY_BAGGAGE; - const propagationContext = propagationContextFromHeaders(sentryTraceEnv, baggageEnv); - getCurrentScope().setPropagationContext(propagationContext); - } -} diff --git a/packages/node-experimental/src/sdk/api.ts b/packages/node/src/sdk/api.ts similarity index 100% rename from packages/node-experimental/src/sdk/api.ts rename to packages/node/src/sdk/api.ts diff --git a/packages/node-experimental/src/sdk/client.ts b/packages/node/src/sdk/client.ts similarity index 100% rename from packages/node-experimental/src/sdk/client.ts rename to packages/node/src/sdk/client.ts diff --git a/packages/node-experimental/src/sdk/init.ts b/packages/node/src/sdk/init.ts similarity index 100% rename from packages/node-experimental/src/sdk/init.ts rename to packages/node/src/sdk/init.ts diff --git a/packages/node-experimental/src/sdk/initOtel.ts b/packages/node/src/sdk/initOtel.ts similarity index 100% rename from packages/node-experimental/src/sdk/initOtel.ts rename to packages/node/src/sdk/initOtel.ts diff --git a/packages/node-experimental/src/sdk/scope.ts b/packages/node/src/sdk/scope.ts similarity index 100% rename from packages/node-experimental/src/sdk/scope.ts rename to packages/node/src/sdk/scope.ts diff --git a/packages/node/src/tracing/index.ts b/packages/node/src/tracing/index.ts deleted file mode 100644 index 2b4be9d41e70..000000000000 --- a/packages/node/src/tracing/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { LazyLoadedIntegration } from '@sentry-internal/tracing'; -import { lazyLoadedNodePerformanceMonitoringIntegrations } from '@sentry-internal/tracing'; -import type { Integration } from '@sentry/types'; -import { logger } from '@sentry/utils'; - -/** - * Automatically detects and returns integrations that will work with your dependencies. - */ -export function autoDiscoverNodePerformanceMonitoringIntegrations(): Integration[] { - const loadedIntegrations = lazyLoadedNodePerformanceMonitoringIntegrations - .map(tryLoad => { - try { - return tryLoad(); - } catch (_) { - return undefined; - } - }) - .filter(integration => !!integration) as LazyLoadedIntegration[]; - - if (loadedIntegrations.length === 0) { - logger.warn('Performance monitoring integrations could not be automatically loaded.'); - } - - // Only return integrations where their dependencies loaded successfully. - return loadedIntegrations.filter(integration => !!integration.loadDependency()); -} diff --git a/packages/node/src/tracing/integrations.ts b/packages/node/src/tracing/integrations.ts deleted file mode 100644 index a37bf6bfd494..000000000000 --- a/packages/node/src/tracing/integrations.ts +++ /dev/null @@ -1 +0,0 @@ -export { Apollo, Express, GraphQL, Mongo, Mysql, Postgres, Prisma } from '@sentry-internal/tracing'; diff --git a/packages/node/src/transports/http-module.ts b/packages/node/src/transports/http-module.ts index 64b255cc869c..f5cbe6fd35f9 100644 --- a/packages/node/src/transports/http-module.ts +++ b/packages/node/src/transports/http-module.ts @@ -1,7 +1,5 @@ -import type { IncomingHttpHeaders, RequestOptions as HTTPRequestOptions } from 'http'; -import type { RequestOptions as HTTPSRequestOptions } from 'https'; -import type { Writable } from 'stream'; -import type { URL } from 'url'; +import type { ClientRequest, IncomingHttpHeaders, RequestOptions as HTTPRequestOptions } from 'node:http'; +import type { RequestOptions as HTTPSRequestOptions } from 'node:https'; export type HTTPModuleRequestOptions = HTTPRequestOptions | HTTPSRequestOptions | string | URL; @@ -26,15 +24,5 @@ export interface HTTPModule { * @param options These are {@see TransportOptions} * @param callback Callback when request is finished */ - request(options: HTTPModuleRequestOptions, callback?: (res: HTTPModuleRequestIncomingMessage) => void): Writable; - - // This is the type for nodejs versions that handle the URL argument - // (v10.9.0+), but we do not use it just yet because we support older node - // versions: - - // request( - // url: string | URL, - // options: http.RequestOptions | https.RequestOptions, - // callback?: (res: http.IncomingMessage) => void, - // ): http.ClientRequest; + request(options: HTTPModuleRequestOptions, callback?: (res: HTTPModuleRequestIncomingMessage) => void): ClientRequest; } diff --git a/packages/node/src/transports/http.ts b/packages/node/src/transports/http.ts index a6a05fc07c95..4cbe7ece1f60 100644 --- a/packages/node/src/transports/http.ts +++ b/packages/node/src/transports/http.ts @@ -1,8 +1,9 @@ import * as http from 'node:http'; import * as https from 'node:https'; import { Readable } from 'stream'; -import { URL } from 'url'; import { createGzip } from 'zlib'; +import { context } from '@opentelemetry/api'; +import { suppressTracing } from '@opentelemetry/core'; import { createTransport } from '@sentry/core'; import type { BaseTransportOptions, @@ -13,7 +14,6 @@ import type { } from '@sentry/types'; import { consoleSandbox } from '@sentry/utils'; import { HttpsProxyAgent } from '../proxy'; - import type { HTTPModule } from './http-module'; export interface NodeTransportOptions extends BaseTransportOptions { @@ -81,8 +81,11 @@ export function makeNodeTransport(options: NodeTransportOptions): Transport { ? (new HttpsProxyAgent(proxy) as http.Agent) : new nativeHttpModule.Agent({ keepAlive, maxSockets: 30, timeout: 2000 }); - const requestExecutor = createRequestExecutor(options, options.httpModule ?? nativeHttpModule, agent); - return createTransport(options, requestExecutor); + // This ensures we do not generate any spans in OpenTelemetry for the transport + return context.with(suppressTracing(context.active()), () => { + const requestExecutor = createRequestExecutor(options, options.httpModule ?? nativeHttpModule, agent); + return createTransport(options, requestExecutor); + }); } /** diff --git a/packages/node/src/types.ts b/packages/node/src/types.ts index 01f91fb46cbe..d78e1761fd79 100644 --- a/packages/node/src/types.ts +++ b/packages/node/src/types.ts @@ -1,6 +1,7 @@ -import type { ClientOptions, Options, SamplingContext, TracePropagationTargets } from '@sentry/types'; +import type { Span as WriteableSpan } from '@opentelemetry/api'; +import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'; +import type { ClientOptions, Options, SamplingContext, Scope, Span, TracePropagationTargets } from '@sentry/types'; -import type { NodeClient } from './client'; import type { NodeTransportOptions } from './transports'; export interface BaseNodeOptions { @@ -51,14 +52,6 @@ export interface BaseNodeOptions { */ includeLocalVariables?: boolean; - /** - * Specify a custom NodeClient to be used. Must extend NodeClient! - * This is not a public, supported API, but used internally only. - * - * @hidden - * */ - clientClass?: typeof NodeClient; - /** * If you use Spotlight by Sentry during development, use * this option to forward captured Sentry events to Spotlight. @@ -71,23 +64,15 @@ export interface BaseNodeOptions { */ spotlight?: boolean | string; - // TODO (v8): Remove this in v8 /** - * @deprecated Moved to constructor options of the `Http` and `Undici` integration. - * @example - * ```js - * Sentry.init({ - * integrations: [ - * new Sentry.Integrations.Http({ - * tracing: { - * shouldCreateSpanForRequest: (url: string) => false, - * } - * }); - * ], - * }); - * ``` + * If this is set to true, the SDK will not set up OpenTelemetry automatically. + * In this case, you _have_ to ensure to set it up correctly yourself, including: + * * The `SentrySpanProcessor` + * * The `SentryPropagator` + * * The `SentryContextManager` + * * The `SentrySampler` */ - shouldCreateSpanForRequest?(this: void, url: string): boolean; + skipOpenTelemetrySetup?: boolean; /** Callback that is executed when a fatal global error occurs. */ onFatalError?(this: void, error: Error): void; @@ -104,3 +89,19 @@ export interface NodeOptions extends Options, BaseNodeOpti * @see NodeClient for more information. */ export interface NodeClientOptions extends ClientOptions, BaseNodeOptions {} + +export interface CurrentScopes { + scope: Scope; + isolationScope: Scope; +} + +/** + * The base `Span` type is basically a `WriteableSpan`. + * There are places where we basically want to allow passing _any_ span, + * so in these cases we type this as `AbstractSpan` which could be either a regular `Span` or a `ReadableSpan`. + * You'll have to make sur to check revelant fields before accessing them. + * + * Note that technically, the `Span` exported from `@opentelemwetry/sdk-trace-base` matches this, + * but we cannot be 100% sure that we are actually getting such a span, so this type is more defensive. + */ +export type AbstractSpan = WriteableSpan | ReadableSpan | Span; diff --git a/packages/node-experimental/src/utils/addOriginToSpan.ts b/packages/node/src/utils/addOriginToSpan.ts similarity index 100% rename from packages/node-experimental/src/utils/addOriginToSpan.ts rename to packages/node/src/utils/addOriginToSpan.ts diff --git a/packages/node-experimental/src/utils/errorhandling.ts b/packages/node/src/utils/errorhandling.ts similarity index 100% rename from packages/node-experimental/src/utils/errorhandling.ts rename to packages/node/src/utils/errorhandling.ts diff --git a/packages/node-experimental/src/utils/getRequestUrl.ts b/packages/node/src/utils/getRequestUrl.ts similarity index 100% rename from packages/node-experimental/src/utils/getRequestUrl.ts rename to packages/node/src/utils/getRequestUrl.ts diff --git a/packages/node-experimental/src/utils/module.ts b/packages/node/src/utils/module.ts similarity index 100% rename from packages/node-experimental/src/utils/module.ts rename to packages/node/src/utils/module.ts diff --git a/packages/node-experimental/src/utils/prepareEvent.ts b/packages/node/src/utils/prepareEvent.ts similarity index 100% rename from packages/node-experimental/src/utils/prepareEvent.ts rename to packages/node/src/utils/prepareEvent.ts diff --git a/packages/node/test/async/domain.test.ts b/packages/node/test/async/domain.test.ts deleted file mode 100644 index 5e06695ed2f6..000000000000 --- a/packages/node/test/async/domain.test.ts +++ /dev/null @@ -1,224 +0,0 @@ -/* eslint-disable deprecation/deprecation */ -import type { Hub } from '@sentry/core'; -import { getCurrentHub, getCurrentScope, setAsyncContextStrategy, withScope } from '@sentry/core'; -import { getIsolationScope, withIsolationScope } from '@sentry/core'; -import type { Scope } from '@sentry/types'; - -import { setDomainAsyncContextStrategy } from '../../src/async/domain'; - -describe('setDomainAsyncContextStrategy()', () => { - beforeEach(() => { - getCurrentScope().clear(); - getIsolationScope().clear(); - }); - - afterEach(() => { - // clear the strategy - setAsyncContextStrategy(undefined); - }); - - describe('with withIsolationScope()', () => { - it('forks the isolation scope (creating a new one)', done => { - expect.assertions(7); - setDomainAsyncContextStrategy(); - - const topLevelIsolationScope = getIsolationScope(); - topLevelIsolationScope.setTag('val1', true); - - withIsolationScope(isolationScope1 => { - expect(isolationScope1).not.toBe(topLevelIsolationScope); - expect(isolationScope1.getScopeData().tags['val1']).toBe(true); - isolationScope1.setTag('val2', true); - topLevelIsolationScope.setTag('val3', true); - - withIsolationScope(isolationScope2 => { - expect(isolationScope2).not.toBe(isolationScope1); - expect(isolationScope2).not.toBe(topLevelIsolationScope); - expect(isolationScope2.getScopeData().tags['val1']).toBe(true); - expect(isolationScope2.getScopeData().tags['val2']).toBe(true); - expect(isolationScope2.getScopeData().tags['val3']).toBeUndefined(); - - done(); - }); - }); - }); - - it('correctly keeps track of isolation scope across asynchronous operations', done => { - expect.assertions(7); - setDomainAsyncContextStrategy(); - - const topLevelIsolationScope = getIsolationScope(); - expect(getIsolationScope()).toBe(topLevelIsolationScope); - - withIsolationScope(isolationScope1 => { - setTimeout(() => { - expect(getIsolationScope()).toBe(isolationScope1); - - withIsolationScope(isolationScope2 => { - setTimeout(() => { - expect(getIsolationScope()).toBe(isolationScope2); - }, 100); - }); - - setTimeout(() => { - expect(getIsolationScope()).toBe(isolationScope1); - done(); - }, 200); - - expect(getIsolationScope()).toBe(isolationScope1); - }, 100); - }); - - setTimeout(() => { - expect(getIsolationScope()).toBe(topLevelIsolationScope); - }, 200); - - expect(getIsolationScope()).toBe(topLevelIsolationScope); - }); - }); - - describe('with withScope()', () => { - test('hub scope inheritance', () => { - setDomainAsyncContextStrategy(); - - const globalHub = getCurrentHub(); - const initialIsolationScope = getIsolationScope(); - const initialScope = getCurrentScope(); - - initialScope.setExtra('a', 'b'); - - withScope(scope => { - const hub1 = getCurrentHub(); - expect(hub1).not.toBe(globalHub); - expect(hub1).toEqual(globalHub); - - expect(hub1.getScope()).toBe(scope); - expect(getCurrentScope()).toBe(scope); - expect(scope).not.toBe(initialScope); - - scope.setExtra('c', 'd'); - - expect(hub1.getIsolationScope()).toBe(initialIsolationScope); - expect(getIsolationScope()).toBe(initialIsolationScope); - - withScope(scope2 => { - const hub2 = getCurrentHub(); - expect(hub2).not.toBe(hub1); - expect(hub2).toEqual(hub1); - expect(hub2).not.toEqual(globalHub); - - expect(scope2).toEqual(scope); - expect(scope2).not.toBe(scope); - - scope.setExtra('e', 'f'); - expect(scope2).not.toEqual(scope); - }); - }); - }); - - test('async hub scope inheritance', async () => { - setDomainAsyncContextStrategy(); - - async function addRandomExtra(scope: Scope, key: string): Promise { - return new Promise(resolve => { - setTimeout(() => { - scope.setExtra(key, Math.random()); - resolve(); - }, 100); - }); - } - - const globalHub = getCurrentHub(); - const initialIsolationScope = getIsolationScope(); - const initialScope = getCurrentScope(); - - await addRandomExtra(initialScope, 'a'); - - await withScope(async scope => { - const hub1 = getCurrentHub(); - expect(hub1).not.toBe(globalHub); - expect(hub1).toEqual(globalHub); - - expect(hub1.getScope()).toBe(scope); - expect(getCurrentScope()).toBe(scope); - expect(scope).not.toBe(initialScope); - - await addRandomExtra(scope, 'b'); - - expect(hub1.getIsolationScope()).toBe(initialIsolationScope); - expect(getIsolationScope()).toBe(initialIsolationScope); - - await withScope(async scope2 => { - const hub2 = getCurrentHub(); - expect(hub2).not.toBe(hub1); - expect(hub2).toEqual(hub1); - expect(hub2).not.toEqual(globalHub); - - expect(scope2).toEqual(scope); - expect(scope2).not.toBe(scope); - - await addRandomExtra(scope2, 'c'); - expect(scope2).not.toEqual(scope); - }); - }); - }); - - test('context single instance', () => { - setDomainAsyncContextStrategy(); - - const globalHub = getCurrentHub(); - withScope(() => { - expect(globalHub).not.toBe(getCurrentHub()); - }); - }); - - test('context within a context not reused', () => { - setDomainAsyncContextStrategy(); - - withScope(() => { - const hub1 = getCurrentHub(); - withScope(() => { - const hub2 = getCurrentHub(); - expect(hub1).not.toBe(hub2); - }); - }); - }); - - test('concurrent hub contexts', done => { - setDomainAsyncContextStrategy(); - - let d1done = false; - let d2done = false; - - withScope(() => { - const hub = getCurrentHub() as Hub; - - hub.getStack().push({ client: 'process' } as any); - - expect(hub.getStack()[1]).toEqual({ client: 'process' }); - // Just in case so we don't have to worry which one finishes first - // (although it always should be d2) - setTimeout(() => { - d1done = true; - if (d2done) { - done(); - } - }, 0); - }); - - withScope(() => { - const hub = getCurrentHub() as Hub; - - hub.getStack().push({ client: 'local' } as any); - - expect(hub.getStack()[1]).toEqual({ client: 'local' }); - setTimeout(() => { - d2done = true; - if (d1done) { - done(); - } - }, 0); - }); - }); - }); -}); diff --git a/packages/node/test/async/hooks.test.ts b/packages/node/test/async/hooks.test.ts deleted file mode 100644 index 8cbd575b4dd6..000000000000 --- a/packages/node/test/async/hooks.test.ts +++ /dev/null @@ -1,230 +0,0 @@ -/* eslint-disable deprecation/deprecation */ -import type { Hub } from '@sentry/core'; -import { - getCurrentHub, - getCurrentScope, - getIsolationScope, - setAsyncContextStrategy, - withIsolationScope, - withScope, -} from '@sentry/core'; -import type { Scope } from '@sentry/types'; - -import { setHooksAsyncContextStrategy } from '../../src/async/hooks'; - -describe('setHooksAsyncContextStrategy()', () => { - beforeEach(() => { - getCurrentScope().clear(); - getIsolationScope().clear(); - }); - - afterEach(() => { - // clear the strategy - setAsyncContextStrategy(undefined); - }); - - describe('with withIsolationScope()', () => { - it('forks the isolation scope (creating a new one)', done => { - expect.assertions(7); - setHooksAsyncContextStrategy(); - - const topLevelIsolationScope = getIsolationScope(); - topLevelIsolationScope.setTag('val1', true); - - withIsolationScope(isolationScope1 => { - expect(isolationScope1).not.toBe(topLevelIsolationScope); - expect(isolationScope1.getScopeData().tags['val1']).toBe(true); - isolationScope1.setTag('val2', true); - topLevelIsolationScope.setTag('val3', true); - - withIsolationScope(isolationScope2 => { - expect(isolationScope2).not.toBe(isolationScope1); - expect(isolationScope2).not.toBe(topLevelIsolationScope); - expect(isolationScope2.getScopeData().tags['val1']).toBe(true); - expect(isolationScope2.getScopeData().tags['val2']).toBe(true); - expect(isolationScope2.getScopeData().tags['val3']).toBeUndefined(); - - done(); - }); - }); - }); - - it('correctly keeps track of isolation scope across asynchronous operations', done => { - expect.assertions(7); - setHooksAsyncContextStrategy(); - - const topLevelIsolationScope = getIsolationScope(); - expect(getIsolationScope()).toBe(topLevelIsolationScope); - - withIsolationScope(isolationScope1 => { - setTimeout(() => { - expect(getIsolationScope()).toBe(isolationScope1); - - withIsolationScope(isolationScope2 => { - setTimeout(() => { - expect(getIsolationScope()).toBe(isolationScope2); - }, 100); - }); - - setTimeout(() => { - expect(getIsolationScope()).toBe(isolationScope1); - done(); - }, 200); - - expect(getIsolationScope()).toBe(isolationScope1); - }, 100); - }); - - setTimeout(() => { - expect(getIsolationScope()).toBe(topLevelIsolationScope); - }, 200); - - expect(getIsolationScope()).toBe(topLevelIsolationScope); - }); - }); - - describe('with withScope()', () => { - test('hub scope inheritance', () => { - setHooksAsyncContextStrategy(); - - const globalHub = getCurrentHub(); - const initialIsolationScope = getIsolationScope(); - const initialScope = getCurrentScope(); - - initialScope.setExtra('a', 'b'); - - withScope(scope => { - const hub1 = getCurrentHub(); - expect(hub1).not.toBe(globalHub); - expect(hub1).toEqual(globalHub); - - expect(hub1.getScope()).toBe(scope); - expect(getCurrentScope()).toBe(scope); - expect(scope).not.toBe(initialScope); - - scope.setExtra('c', 'd'); - - expect(hub1.getIsolationScope()).toBe(initialIsolationScope); - expect(getIsolationScope()).toBe(initialIsolationScope); - - withScope(scope2 => { - const hub2 = getCurrentHub(); - expect(hub2).not.toBe(hub1); - expect(hub2).toEqual(hub1); - expect(hub2).not.toEqual(globalHub); - - expect(scope2).toEqual(scope); - expect(scope2).not.toBe(scope); - - scope.setExtra('e', 'f'); - expect(scope2).not.toEqual(scope); - }); - }); - }); - - test('async hub scope inheritance', async () => { - setHooksAsyncContextStrategy(); - - async function addRandomExtra(scope: Scope, key: string): Promise { - return new Promise(resolve => { - setTimeout(() => { - scope.setExtra(key, Math.random()); - resolve(); - }, 100); - }); - } - - const globalHub = getCurrentHub(); - const initialIsolationScope = getIsolationScope(); - const initialScope = getCurrentScope(); - - await addRandomExtra(initialScope, 'a'); - - await withScope(async scope => { - const hub1 = getCurrentHub(); - expect(hub1).not.toBe(globalHub); - expect(hub1).toEqual(globalHub); - - expect(hub1.getScope()).toBe(scope); - expect(getCurrentScope()).toBe(scope); - expect(scope).not.toBe(initialScope); - - await addRandomExtra(scope, 'b'); - - expect(hub1.getIsolationScope()).toBe(initialIsolationScope); - expect(getIsolationScope()).toBe(initialIsolationScope); - - await withScope(async scope2 => { - const hub2 = getCurrentHub(); - expect(hub2).not.toBe(hub1); - expect(hub2).toEqual(hub1); - expect(hub2).not.toEqual(globalHub); - - expect(scope2).toEqual(scope); - expect(scope2).not.toBe(scope); - - await addRandomExtra(scope2, 'c'); - expect(scope2).not.toEqual(scope); - }); - }); - }); - - test('context single instance', () => { - setHooksAsyncContextStrategy(); - - const globalHub = getCurrentHub(); - withScope(() => { - expect(globalHub).not.toBe(getCurrentHub()); - }); - }); - - test('context within a context not reused', () => { - setHooksAsyncContextStrategy(); - - withScope(() => { - const hub1 = getCurrentHub(); - withScope(() => { - const hub2 = getCurrentHub(); - expect(hub1).not.toBe(hub2); - }); - }); - }); - - test('concurrent hub contexts', done => { - setHooksAsyncContextStrategy(); - - let d1done = false; - let d2done = false; - - withScope(() => { - const hub = getCurrentHub() as Hub; - - hub.getStack().push({ client: 'process' } as any); - - expect(hub.getStack()[1]).toEqual({ client: 'process' }); - // Just in case so we don't have to worry which one finishes first - // (although it always should be d2) - setTimeout(() => { - d1done = true; - if (d2done) { - done(); - } - }, 0); - }); - - withScope(() => { - const hub = getCurrentHub() as Hub; - - hub.getStack().push({ client: 'local' } as any); - - expect(hub.getStack()[1]).toEqual({ client: 'local' }); - setTimeout(() => { - d2done = true; - if (d1done) { - done(); - } - }, 0); - }); - }); - }); -}); diff --git a/packages/node/test/client.test.ts b/packages/node/test/client.test.ts deleted file mode 100644 index b80c3eced700..000000000000 --- a/packages/node/test/client.test.ts +++ /dev/null @@ -1,440 +0,0 @@ -import * as os from 'os'; -import { SessionFlusher, getCurrentScope, getGlobalScope, getIsolationScope, withIsolationScope } from '@sentry/core'; -import type { Event, EventHint } from '@sentry/types'; - -import type { Scope } from '@sentry/types'; -import { NodeClient } from '../src'; -import { setNodeAsyncContextStrategy } from '../src/async'; -import { getDefaultNodeClientOptions } from './helper/node-client-options'; - -const PUBLIC_DSN = 'https://username@domain/123'; - -describe('NodeClient', () => { - let client: NodeClient; - - afterEach(() => { - if ('_sessionFlusher' in client) clearInterval((client as any)._sessionFlusher._intervalId); - jest.restoreAllMocks(); - - getIsolationScope().clear(); - getGlobalScope().clear(); - getCurrentScope().clear(); - getCurrentScope().setClient(undefined); - }); - - beforeEach(() => { - setNodeAsyncContextStrategy(); - }); - - describe('captureException', () => { - test('when autoSessionTracking is enabled, and requestHandler is not used -> requestStatus should not be set', () => { - const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.4' }); - client = new NodeClient(options); - - withIsolationScope(isolationScope => { - isolationScope.setRequestSession({ status: 'ok' }); - - client.captureException(new Error('test exception')); - - const requestSession = isolationScope.getRequestSession(); - expect(requestSession!.status).toEqual('ok'); - }); - }); - - test('when autoSessionTracking is disabled -> requestStatus should not be set', () => { - const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: false, release: '1.4' }); - client = new NodeClient(options); - // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised - // by the`requestHandler`) - client.initSessionFlusher(); - - withIsolationScope(isolationScope => { - isolationScope.setRequestSession({ status: 'ok' }); - - client.captureException(new Error('test exception')); - - const requestSession = isolationScope.getRequestSession(); - expect(requestSession!.status).toEqual('ok'); - }); - }); - - test('when autoSessionTracking is enabled + requestSession status is Crashed -> requestStatus should not be overridden', () => { - const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.4' }); - client = new NodeClient(options); - // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised - // by the`requestHandler`) - client.initSessionFlusher(); - - withIsolationScope(isolationScope => { - isolationScope.setRequestSession({ status: 'crashed' }); - - client.captureException(new Error('test exception')); - - const requestSession = isolationScope.getRequestSession(); - expect(requestSession!.status).toEqual('crashed'); - }); - }); - - test('when autoSessionTracking is enabled + error occurs within request bounds -> requestStatus should be set to Errored', () => { - const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.4' }); - client = new NodeClient(options); - // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised - // by the`requestHandler`) - client.initSessionFlusher(); - - withIsolationScope(isolationScope => { - isolationScope.setRequestSession({ status: 'ok' }); - - client.captureException(new Error('test exception')); - - const requestSession = isolationScope.getRequestSession(); - expect(requestSession!.status).toEqual('errored'); - }); - }); - - test('when autoSessionTracking is enabled + error occurs outside of request bounds -> requestStatus should not be set to Errored', done => { - const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.4' }); - client = new NodeClient(options); - - // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised - // by the`requestHandler`) - client.initSessionFlusher(); - - let isolationScope: Scope; - withIsolationScope(_isolationScope => { - _isolationScope.setRequestSession({ status: 'ok' }); - isolationScope = _isolationScope; - }); - - client.captureException(new Error('test exception')); - - setImmediate(() => { - const requestSession = isolationScope.getRequestSession(); - expect(requestSession).toEqual({ status: 'ok' }); - done(); - }); - }); - }); - - describe('captureEvent()', () => { - test('If autoSessionTracking is disabled, requestSession status should not be set', () => { - const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: false, release: '1.4' }); - client = new NodeClient(options); - // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised - // by the`requestHandler`) - client.initSessionFlusher(); - - withIsolationScope(isolationScope => { - isolationScope.setRequestSession({ status: 'ok' }); - client.captureEvent({ message: 'message', exception: { values: [{ type: 'exception type 1' }] } }); - const requestSession = isolationScope.getRequestSession(); - expect(requestSession!.status).toEqual('ok'); - }); - }); - - test('When captureEvent is called with an exception, requestSession status should be set to Errored', () => { - const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '2.2' }); - client = new NodeClient(options); - // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised - // by the`requestHandler`) - client.initSessionFlusher(); - - withIsolationScope(isolationScope => { - isolationScope.setRequestSession({ status: 'ok' }); - - client.captureEvent({ message: 'message', exception: { values: [{ type: 'exception type 1' }] } }); - - const requestSession = isolationScope.getRequestSession(); - expect(requestSession!.status).toEqual('errored'); - }); - }); - - test('When captureEvent is called without an exception, requestSession status should not be set to Errored', () => { - const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '2.2' }); - client = new NodeClient(options); - // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised - // by the`requestHandler`) - client.initSessionFlusher(); - - withIsolationScope(isolationScope => { - isolationScope.setRequestSession({ status: 'ok' }); - - client.captureEvent({ message: 'message' }); - - const requestSession = isolationScope.getRequestSession(); - expect(requestSession!.status).toEqual('ok'); - }); - }); - - test('When captureEvent is called with an exception but outside of a request, then requestStatus should not be set', () => { - const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '2.2' }); - client = new NodeClient(options); - // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised - // by the`requestHandler`) - client.initSessionFlusher(); - - withIsolationScope(isolationScope => { - isolationScope.clear(); - client.captureEvent({ message: 'message', exception: { values: [{ type: 'exception type 1' }] } }); - - expect(isolationScope.getRequestSession()).toEqual(undefined); - }); - }); - - test('When captureEvent is called with a transaction, then requestSession status should not be set', () => { - const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.3' }); - client = new NodeClient(options); - // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised - // by the`requestHandler`) - client.initSessionFlusher(); - - withIsolationScope(isolationScope => { - isolationScope.setRequestSession({ status: 'ok' }); - - client.captureEvent({ message: 'message', type: 'transaction' }); - - const requestSession = isolationScope.getRequestSession(); - expect(requestSession!.status).toEqual('ok'); - }); - }); - - test('When captureEvent is called with an exception but requestHandler is not used, then requestSession status should not be set', () => { - const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.3' }); - client = new NodeClient(options); - - withIsolationScope(isolationScope => { - isolationScope.setRequestSession({ status: 'ok' }); - - client.captureEvent({ message: 'message', exception: { values: [{ type: 'exception type 1' }] } }); - - const requestSession = isolationScope.getRequestSession(); - expect(requestSession!.status).toEqual('ok'); - }); - }); - }); - - describe('_prepareEvent', () => { - test('adds platform to event', () => { - const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN }); - client = new NodeClient(options); - - const event: Event = {}; - const hint: EventHint = {}; - (client as any)._prepareEvent(event, hint); - - expect(event.platform).toEqual('node'); - }); - - test('adds runtime context to event', () => { - const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN }); - client = new NodeClient(options); - - const event: Event = {}; - const hint: EventHint = {}; - (client as any)._prepareEvent(event, hint); - - expect(event.contexts?.runtime).toEqual({ - name: 'node', - version: process.version, - }); - }); - - test('adds server name to event when value passed in options', () => { - const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, serverName: 'foo' }); - client = new NodeClient(options); - - const event: Event = {}; - const hint: EventHint = {}; - (client as any)._prepareEvent(event, hint); - - expect(event.server_name).toEqual('foo'); - }); - - test('adds server name to event when value given in env', () => { - const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN }); - process.env.SENTRY_NAME = 'foo'; - client = new NodeClient(options); - - const event: Event = {}; - const hint: EventHint = {}; - (client as any)._prepareEvent(event, hint); - - expect(event.server_name).toEqual('foo'); - - delete process.env.SENTRY_NAME; - }); - - test('adds hostname as event server name when no value given', () => { - const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN }); - client = new NodeClient(options); - - const event: Event = {}; - const hint: EventHint = {}; - (client as any)._prepareEvent(event, hint); - - expect(event.server_name).toEqual(os.hostname()); - }); - - test("doesn't clobber existing runtime data", () => { - const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, serverName: 'bar' }); - client = new NodeClient(options); - - const event: Event = { contexts: { runtime: { name: 'foo', version: '1.2.3' } } }; - const hint: EventHint = {}; - (client as any)._prepareEvent(event, hint); - - expect(event.contexts?.runtime).toEqual({ name: 'foo', version: '1.2.3' }); - expect(event.contexts?.runtime).not.toEqual({ name: 'node', version: process.version }); - }); - - test("doesn't clobber existing server name", () => { - const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, serverName: 'bar' }); - client = new NodeClient(options); - - const event: Event = { server_name: 'foo' }; - const hint: EventHint = {}; - (client as any)._prepareEvent(event, hint); - - expect(event.server_name).toEqual('foo'); - expect(event.server_name).not.toEqual('bar'); - }); - }); - - describe('captureCheckIn', () => { - it('sends a checkIn envelope', () => { - const options = getDefaultNodeClientOptions({ - dsn: PUBLIC_DSN, - serverName: 'bar', - release: '1.0.0', - environment: 'dev', - }); - client = new NodeClient(options); - - const sendEnvelopeSpy = jest.spyOn(client, 'sendEnvelope'); - - const id = client.captureCheckIn( - { monitorSlug: 'foo', status: 'in_progress' }, - { - schedule: { - type: 'crontab', - value: '0 * * * *', - }, - checkinMargin: 2, - maxRuntime: 12333, - timezone: 'Canada/Eastern', - }, - ); - - expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1); - expect(sendEnvelopeSpy).toHaveBeenCalledWith([ - expect.any(Object), - [ - [ - expect.any(Object), - { - check_in_id: id, - monitor_slug: 'foo', - status: 'in_progress', - release: '1.0.0', - environment: 'dev', - monitor_config: { - schedule: { - type: 'crontab', - value: '0 * * * *', - }, - checkin_margin: 2, - max_runtime: 12333, - timezone: 'Canada/Eastern', - }, - }, - ], - ], - ]); - - client.captureCheckIn({ monitorSlug: 'foo', status: 'ok', duration: 1222, checkInId: id }); - - expect(sendEnvelopeSpy).toHaveBeenCalledTimes(2); - expect(sendEnvelopeSpy).toHaveBeenCalledWith([ - expect.any(Object), - [ - [ - expect.any(Object), - { - check_in_id: id, - monitor_slug: 'foo', - duration: 1222, - status: 'ok', - release: '1.0.0', - environment: 'dev', - }, - ], - ], - ]); - }); - - it('sends a checkIn envelope for heartbeat checkIns', () => { - const options = getDefaultNodeClientOptions({ - dsn: PUBLIC_DSN, - serverName: 'server', - release: '1.0.0', - environment: 'dev', - }); - client = new NodeClient(options); - - const sendEnvelopeSpy = jest.spyOn(client, 'sendEnvelope'); - - const id = client.captureCheckIn({ monitorSlug: 'heartbeat-monitor', status: 'ok' }); - - expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1); - expect(sendEnvelopeSpy).toHaveBeenCalledWith([ - expect.any(Object), - [ - [ - expect.any(Object), - { - check_in_id: id, - monitor_slug: 'heartbeat-monitor', - status: 'ok', - release: '1.0.0', - environment: 'dev', - }, - ], - ], - ]); - }); - - it('does not send a checkIn envelope if disabled', () => { - const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, serverName: 'bar', enabled: false }); - client = new NodeClient(options); - - const sendEnvelopeSpy = jest.spyOn(client, 'sendEnvelope'); - - client.captureCheckIn({ monitorSlug: 'foo', status: 'in_progress' }); - - expect(sendEnvelopeSpy).toHaveBeenCalledTimes(0); - }); - }); -}); - -describe('flush/close', () => { - test('client close function disables _sessionFlusher', async () => { - jest.useRealTimers(); - const options = getDefaultNodeClientOptions({ - dsn: PUBLIC_DSN, - autoSessionTracking: true, - release: '1.1', - }); - const client = new NodeClient(options); - client.initSessionFlusher(); - // Clearing interval is important here to ensure that the flush function later on is called by the `client.close()` - // not due to the interval running every 60s - clearInterval((client as any)._sessionFlusher._intervalId); - - const sessionFlusherFlushFunc = jest.spyOn(SessionFlusher.prototype, 'flush'); - - const delay = 1; - await client.close(delay); - expect((client as any)._sessionFlusher._isEnabled).toBeFalsy(); - expect(sessionFlusherFlushFunc).toHaveBeenCalledTimes(1); - }); -}); diff --git a/packages/node/test/eventbuilders.test.ts b/packages/node/test/eventbuilders.test.ts deleted file mode 100644 index 8afc1537c082..000000000000 --- a/packages/node/test/eventbuilders.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { eventFromUnknownInput } from '@sentry/utils'; - -import { defaultStackParser } from '../src'; - -describe('eventFromUnknownInput', () => { - test('uses normalizeDepth from init options', () => { - const deepObject = { - a: { - b: { - c: { - d: { - e: { - f: { - g: 'foo', - }, - }, - }, - }, - }, - }, - }; - - const client = { - getOptions(): any { - return { normalizeDepth: 6 }; - }, - } as any; - const event = eventFromUnknownInput(client, defaultStackParser, deepObject); - - const serializedObject = event.extra?.__serialized__; - expect(serializedObject).toBeDefined(); - expect(serializedObject).toEqual({ - a: { - b: { - c: { - d: { - e: { - f: '[Object]', - }, - }, - }, - }, - }, - }); - }); -}); diff --git a/packages/node/test/fixtures/testDeepReadDirSync/cats/eddy.txt b/packages/node/test/fixtures/testDeepReadDirSync/cats/eddy.txt deleted file mode 100644 index a34113db4801..000000000000 --- a/packages/node/test/fixtures/testDeepReadDirSync/cats/eddy.txt +++ /dev/null @@ -1 +0,0 @@ -Lived to be almost 23. Named Eddy because she would sometimes just spin around and around and around, for no reason. diff --git a/packages/node/test/fixtures/testDeepReadDirSync/cats/persephone.txt b/packages/node/test/fixtures/testDeepReadDirSync/cats/persephone.txt deleted file mode 100644 index ef98cb18d7ba..000000000000 --- a/packages/node/test/fixtures/testDeepReadDirSync/cats/persephone.txt +++ /dev/null @@ -1 +0,0 @@ -Originally a stray. Adopted the humans rather than vice-versa. diff --git a/packages/node/test/fixtures/testDeepReadDirSync/cats/piper.txt b/packages/node/test/fixtures/testDeepReadDirSync/cats/piper.txt deleted file mode 100644 index 0e3fa7aca948..000000000000 --- a/packages/node/test/fixtures/testDeepReadDirSync/cats/piper.txt +++ /dev/null @@ -1 +0,0 @@ -A polydactyl with an enormous fluffy tail. diff --git a/packages/node/test/fixtures/testDeepReadDirSync/cats/sassafras.txt b/packages/node/test/fixtures/testDeepReadDirSync/cats/sassafras.txt deleted file mode 100644 index 2fd44c1fba33..000000000000 --- a/packages/node/test/fixtures/testDeepReadDirSync/cats/sassafras.txt +++ /dev/null @@ -1 +0,0 @@ -All black. Once ran away for two weeks, but eventually came back. diff --git a/packages/node/test/fixtures/testDeepReadDirSync/cats/teaberry.txt b/packages/node/test/fixtures/testDeepReadDirSync/cats/teaberry.txt deleted file mode 100644 index 83e4df4bb879..000000000000 --- a/packages/node/test/fixtures/testDeepReadDirSync/cats/teaberry.txt +++ /dev/null @@ -1 +0,0 @@ -Named by popular consensus, after the plant that makes wintergreen. diff --git a/packages/node/test/fixtures/testDeepReadDirSync/debra.txt b/packages/node/test/fixtures/testDeepReadDirSync/debra.txt deleted file mode 100644 index b7cd5d098fed..000000000000 --- a/packages/node/test/fixtures/testDeepReadDirSync/debra.txt +++ /dev/null @@ -1 +0,0 @@ -A black and white hooded rat, who loved to eat pizza crusts. diff --git a/packages/node/test/fixtures/testDeepReadDirSync/dogs/theBigs/charlie.txt b/packages/node/test/fixtures/testDeepReadDirSync/dogs/theBigs/charlie.txt deleted file mode 100644 index c51ee65682db..000000000000 --- a/packages/node/test/fixtures/testDeepReadDirSync/dogs/theBigs/charlie.txt +++ /dev/null @@ -1 +0,0 @@ -Named after the Charles River. A big dog who loves to play with tiny dogs. diff --git a/packages/node/test/fixtures/testDeepReadDirSync/dogs/theBigs/maisey.txt b/packages/node/test/fixtures/testDeepReadDirSync/dogs/theBigs/maisey.txt deleted file mode 100644 index 29d690041354..000000000000 --- a/packages/node/test/fixtures/testDeepReadDirSync/dogs/theBigs/maisey.txt +++ /dev/null @@ -1 +0,0 @@ -Has fluff between her toes. Slow to warm, but incredibly loyal thereafter. diff --git a/packages/node/test/fixtures/testDeepReadDirSync/dogs/theSmalls/bodhi.txt b/packages/node/test/fixtures/testDeepReadDirSync/dogs/theSmalls/bodhi.txt deleted file mode 100644 index e4b2ff5e6c4c..000000000000 --- a/packages/node/test/fixtures/testDeepReadDirSync/dogs/theSmalls/bodhi.txt +++ /dev/null @@ -1 +0,0 @@ -Loves to explore. Has spots on his tongue. diff --git a/packages/node/test/fixtures/testDeepReadDirSync/dogs/theSmalls/cory.txt b/packages/node/test/fixtures/testDeepReadDirSync/dogs/theSmalls/cory.txt deleted file mode 100644 index c581fffbf1e1..000000000000 --- a/packages/node/test/fixtures/testDeepReadDirSync/dogs/theSmalls/cory.txt +++ /dev/null @@ -1 +0,0 @@ -Resembles a small sheep. Sneezes when playing with another dog. diff --git a/packages/node/test/handlers.test.ts b/packages/node/test/handlers.test.ts deleted file mode 100644 index 239b79c52564..000000000000 --- a/packages/node/test/handlers.test.ts +++ /dev/null @@ -1,625 +0,0 @@ -import * as http from 'http'; -import * as sentryCore from '@sentry/core'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - Transaction, - getClient, - getCurrentScope, - getIsolationScope, - getMainCarrier, - mergeScopeData, - setCurrentClient, - spanIsSampled, - spanToJSON, - withScope, -} from '@sentry/core'; -import type { Event, PropagationContext, Scope } from '@sentry/types'; -import { SentryError } from '@sentry/utils'; - -import { NodeClient } from '../src/client'; -import { errorHandler, requestHandler, tracingHandler } from '../src/handlers'; -import { getDefaultNodeClientOptions } from './helper/node-client-options'; - -describe('requestHandler', () => { - beforeEach(() => { - getCurrentScope().clear(); - getIsolationScope().clear(); - - // Ensure we reset a potentially set acs to use the default - const sentry = getMainCarrier().__SENTRY__; - if (sentry) { - sentry.acs = undefined; - } - }); - - const headers = { ears: 'furry', nose: 'wet', tongue: 'spotted', cookie: 'favorite=zukes' }; - const method = 'wagging'; - const protocol = 'mutualsniffing'; - const hostname = 'the.dog.park'; - const path = '/by/the/trees/'; - const queryString = 'chase=me&please=thankyou'; - - const sentryRequestMiddleware = requestHandler(); - - let req: http.IncomingMessage, res: http.ServerResponse, next: () => undefined; - - function createNoOpSpy() { - const noop = { noop: () => undefined }; // this is wrapped in an object so jest can spy on it - return jest.spyOn(noop, 'noop') as any; - } - - beforeEach(() => { - req = { - headers, - method, - protocol, - hostname, - originalUrl: `${path}?${queryString}`, - } as unknown as http.IncomingMessage; - res = new http.ServerResponse(req); - next = createNoOpSpy(); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('autoSessionTracking is enabled, sets requestSession status to ok, when handling a request', done => { - const options = getDefaultNodeClientOptions({ autoSessionTracking: true, release: '1.2' }); - const client = new NodeClient(options); - setCurrentClient(client); - - let isolationScope: Scope; - sentryRequestMiddleware(req, res, () => { - isolationScope = getIsolationScope(); - return next(); - }); - - setImmediate(() => { - expect(isolationScope.getRequestSession()).toEqual({ status: 'ok' }); - done(); - }); - }); - - it('autoSessionTracking is disabled, does not set requestSession, when handling a request', done => { - const options = getDefaultNodeClientOptions({ autoSessionTracking: false, release: '1.2' }); - const client = new NodeClient(options); - setCurrentClient(client); - - let isolationScope: Scope; - sentryRequestMiddleware(req, res, () => { - isolationScope = getIsolationScope(); - return next(); - }); - - setImmediate(() => { - expect(isolationScope.getRequestSession()).toEqual(undefined); - done(); - }); - }); - - it('autoSessionTracking is enabled, calls _captureRequestSession, on response finish', done => { - const options = getDefaultNodeClientOptions({ autoSessionTracking: true, release: '1.2' }); - const client = new NodeClient(options); - setCurrentClient(client); - - const captureRequestSession = jest.spyOn(client, '_captureRequestSession'); - - let isolationScope: Scope; - sentryRequestMiddleware(req, res, () => { - isolationScope = getIsolationScope(); - return next(); - }); - - res.emit('finish'); - - setImmediate(() => { - expect(isolationScope.getRequestSession()).toEqual({ status: 'ok' }); - expect(captureRequestSession).toHaveBeenCalled(); - done(); - }); - }); - - it('autoSessionTracking is disabled, does not call _captureRequestSession, on response finish', done => { - const options = getDefaultNodeClientOptions({ autoSessionTracking: false, release: '1.2' }); - const client = new NodeClient(options); - setCurrentClient(client); - - const captureRequestSession = jest.spyOn(client, '_captureRequestSession'); - - let isolationScope: Scope; - sentryRequestMiddleware(req, res, () => { - isolationScope = getIsolationScope(); - return next(); - }); - - res.emit('finish'); - - setImmediate(() => { - expect(isolationScope.getRequestSession()).toBeUndefined(); - expect(captureRequestSession).not.toHaveBeenCalled(); - done(); - }); - }); - - it('patches `res.end` when `flushTimeout` is specified', done => { - const flush = jest.spyOn(sentryCore, 'flush').mockResolvedValue(true); - - const sentryRequestMiddleware = requestHandler({ flushTimeout: 1337 }); - sentryRequestMiddleware(req, res, next); - res.end('ok'); - - setImmediate(() => { - expect(flush).toHaveBeenCalledWith(1337); - // eslint-disable-next-line deprecation/deprecation - expect(res.finished).toBe(true); - done(); - }); - }); - - it('prevents errors thrown during `flush` from breaking the response', done => { - jest.spyOn(sentryCore, 'flush').mockRejectedValue(new SentryError('HTTP Error (429)')); - - const sentryRequestMiddleware = requestHandler({ flushTimeout: 1337 }); - sentryRequestMiddleware(req, res, next); - res.end('ok'); - - setImmediate(() => { - // eslint-disable-next-line deprecation/deprecation - expect(res.finished).toBe(true); - done(); - }); - }); - - it('stores request and request data options in `sdkProcessingMetadata`', done => { - const client = new NodeClient(getDefaultNodeClientOptions()); - setCurrentClient(client); - - const requestHandlerOptions = { include: { ip: false } }; - const sentryRequestMiddleware = requestHandler(requestHandlerOptions); - - let isolationScope: Scope; - let currentScope: Scope; - sentryRequestMiddleware(req, res, () => { - isolationScope = getIsolationScope(); - currentScope = getCurrentScope(); - return next(); - }); - - setImmediate(() => { - const scopeData = isolationScope.getScopeData(); - mergeScopeData(scopeData, currentScope.getScopeData()); - - expect(scopeData.sdkProcessingMetadata).toEqual({ - request: req, - }); - done(); - }); - }); -}); - -describe('tracingHandler', () => { - beforeEach(() => { - getCurrentScope().clear(); - getIsolationScope().clear(); - - // Ensure we reset a potentially set acs to use the default - const sentry = getMainCarrier().__SENTRY__; - if (sentry) { - sentry.acs = undefined; - } - }); - - const headers = { ears: 'furry', nose: 'wet', tongue: 'spotted', cookie: 'favorite=zukes' }; - const method = 'wagging'; - const protocol = 'mutualsniffing'; - const hostname = 'the.dog.park'; - const path = '/by/the/trees/'; - const queryString = 'chase=me&please=thankyou'; - const fragment = '#adoptnotbuy'; - - const sentryTracingMiddleware = tracingHandler(); - - let req: http.IncomingMessage, res: http.ServerResponse, next: () => undefined; - - function createNoOpSpy() { - const noop = { noop: () => undefined }; // this is wrapped in an object so jest can spy on it - return jest.spyOn(noop, 'noop') as any; - } - - beforeEach(() => { - const client = new NodeClient(getDefaultNodeClientOptions({ tracesSampleRate: 1.0 })); - setCurrentClient(client); - - req = { - headers, - method, - protocol, - hostname, - originalUrl: `${path}?${queryString}`, - } as unknown as http.IncomingMessage; - res = new http.ServerResponse(req); - next = createNoOpSpy(); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - function getPropagationContext(): PropagationContext { - return getCurrentScope().getScopeData().propagationContext; - } - - it('creates a transaction when handling a request', () => { - const startInactiveSpan = jest.spyOn(sentryCore, 'startInactiveSpan'); - - sentryTracingMiddleware(req, res, next); - - expect(startInactiveSpan).toHaveBeenCalled(); - }); - - it("doesn't create a transaction when handling a `HEAD` request", () => { - const startInactiveSpan = jest.spyOn(sentryCore, 'startInactiveSpan'); - req.method = 'HEAD'; - - sentryTracingMiddleware(req, res, next); - - expect(startInactiveSpan).not.toHaveBeenCalled(); - }); - - it("doesn't create a transaction when handling an `OPTIONS` request", () => { - const startInactiveSpan = jest.spyOn(sentryCore, 'startInactiveSpan'); - req.method = 'OPTIONS'; - - sentryTracingMiddleware(req, res, next); - - expect(startInactiveSpan).not.toHaveBeenCalled(); - }); - - it("doesn't create a transaction if tracing is disabled", () => { - delete getClient()?.getOptions().tracesSampleRate; - const startInactiveSpan = jest.spyOn(sentryCore, 'startInactiveSpan'); - - sentryTracingMiddleware(req, res, next); - - expect(startInactiveSpan).not.toHaveBeenCalled(); - }); - - it("pulls parent's data from tracing header on the request", () => { - req.headers = { 'sentry-trace': '12312012123120121231201212312012-1121201211212012-0' }; - - sentryTracingMiddleware(req, res, next); - - const transaction = (res as any).__sentry_transaction as Transaction; - - expect(getPropagationContext()).toEqual({ - traceId: '12312012123120121231201212312012', - parentSpanId: '1121201211212012', - spanId: expect.any(String), - sampled: false, - dsc: {}, // There is an incoming trace but no baggage header, so the DSC must be frozen (empty object) - }); - - // since we have no tracesSampler defined, the default behavior (inherit if possible) applies - expect(transaction.spanContext().traceId).toEqual('12312012123120121231201212312012'); - expect(spanToJSON(transaction).parent_span_id).toEqual('1121201211212012'); - expect(spanIsSampled(transaction)).toEqual(false); - // eslint-disable-next-line deprecation/deprecation - expect(transaction.metadata?.dynamicSamplingContext).toStrictEqual({}); - }); - - it("pulls parent's data from tracing and baggage headers on the request", () => { - req.headers = { - 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', - baggage: 'sentry-version=1.0,sentry-environment=production', - }; - - sentryTracingMiddleware(req, res, next); - - expect(getPropagationContext()).toEqual({ - traceId: '12312012123120121231201212312012', - parentSpanId: '1121201211212012', - spanId: expect.any(String), - sampled: true, - dsc: { version: '1.0', environment: 'production' }, - }); - - const transaction = (res as any).__sentry_transaction as Transaction; - - // since we have no tracesSampler defined, the default behavior (inherit if possible) applies - expect(transaction.spanContext().traceId).toEqual('12312012123120121231201212312012'); - expect(spanToJSON(transaction).parent_span_id).toEqual('1121201211212012'); - expect(spanIsSampled(transaction)).toEqual(true); - // eslint-disable-next-line deprecation/deprecation - expect(transaction.metadata?.dynamicSamplingContext).toStrictEqual({ version: '1.0', environment: 'production' }); - }); - - it("doesn't populate dynamic sampling context with 3rd party baggage", () => { - req.headers = { - 'sentry-trace': '12312012123120121231201212312012-1121201211212012-0', - baggage: 'sentry-version=1.0,sentry-environment=production,dogs=great,cats=boring', - }; - - sentryTracingMiddleware(req, res, next); - - expect(getPropagationContext().dsc).toEqual({ version: '1.0', environment: 'production' }); - - const transaction = (res as any).__sentry_transaction as Transaction; - // eslint-disable-next-line deprecation/deprecation - expect(transaction.metadata?.dynamicSamplingContext).toStrictEqual({ version: '1.0', environment: 'production' }); - }); - - it('puts its transaction on the scope', () => { - const options = getDefaultNodeClientOptions({ tracesSampleRate: 1.0 }); - const client = new NodeClient(options); - setCurrentClient(client); - - sentryTracingMiddleware(req, res, next); - - // eslint-disable-next-line deprecation/deprecation - const transaction = getCurrentScope().getTransaction(); - - expect(transaction).toBeDefined(); - const transactionJson = spanToJSON(transaction as Transaction); - expect(transactionJson.description).toEqual(`${method.toUpperCase()} ${path}`); - expect(transactionJson.data?.[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual('http.server'); - }); - - it('puts its transaction on the response object', () => { - sentryTracingMiddleware(req, res, next); - - const transaction = (res as any).__sentry_transaction as Transaction; - - expect(transaction).toBeDefined(); - - const transactionJson = spanToJSON(transaction); - expect(transactionJson.description).toEqual(`${method.toUpperCase()} ${path}`); - expect(transactionJson.data?.[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual('http.server'); - }); - - it('pulls status code from the response', done => { - // eslint-disable-next-line deprecation/deprecation - const transaction = new Transaction({ name: 'mockTransaction' }); - jest.spyOn(sentryCore, 'startInactiveSpan').mockReturnValue(transaction as Transaction); - const finishTransaction = jest.spyOn(transaction, 'end'); - - sentryTracingMiddleware(req, res, next); - res.statusCode = 200; - res.emit('finish'); - - setImmediate(() => { - expect(finishTransaction).toHaveBeenCalled(); - expect(spanToJSON(transaction).status).toBe('ok'); - expect(spanToJSON(transaction).data).toEqual(expect.objectContaining({ 'http.response.status_code': 200 })); - done(); - }); - }); - - it('strips query string from request path', () => { - req.url = `${path}?${queryString}`; - - sentryTracingMiddleware(req, res, next); - - const transaction = (res as any).__sentry_transaction as Transaction; - - expect(spanToJSON(transaction).description).toBe(`${method.toUpperCase()} ${path}`); - }); - - it('strips fragment from request path', () => { - req.url = `${path}${fragment}`; - - sentryTracingMiddleware(req, res, next); - - const transaction = (res as any).__sentry_transaction as Transaction; - - expect(spanToJSON(transaction).description).toBe(`${method.toUpperCase()} ${path}`); - }); - - it('strips query string and fragment from request path', () => { - req.url = `${path}?${queryString}${fragment}`; - - sentryTracingMiddleware(req, res, next); - - const transaction = (res as any).__sentry_transaction as Transaction; - - expect(spanToJSON(transaction).description).toBe(`${method.toUpperCase()} ${path}`); - }); - - it('closes the transaction when request processing is done', done => { - // eslint-disable-next-line deprecation/deprecation - const transaction = new Transaction({ name: 'mockTransaction' }); - jest.spyOn(sentryCore, 'startInactiveSpan').mockReturnValue(transaction as Transaction); - const finishTransaction = jest.spyOn(transaction, 'end'); - - sentryTracingMiddleware(req, res, next); - res.emit('finish'); - - setImmediate(() => { - expect(finishTransaction).toHaveBeenCalled(); - done(); - }); - }); - - it('waits to finish transaction until all spans are finished, even though `transaction.end()` is registered on `res.finish` event first', done => { - // eslint-disable-next-line deprecation/deprecation - const transaction = new Transaction({ name: 'mockTransaction', sampled: true }); - // eslint-disable-next-line deprecation/deprecation - const span = transaction.startChild({ - name: 'reallyCoolHandler', - op: 'middleware', - }); - jest.spyOn(sentryCore, 'startInactiveSpan').mockReturnValue(transaction as Transaction); - const finishSpan = jest.spyOn(span, 'end'); - const finishTransaction = jest.spyOn(transaction, 'end'); - - let sentEvent: Event; - jest.spyOn((transaction as any)._hub, 'captureEvent').mockImplementation(event => { - sentEvent = event as Event; - }); - - sentryTracingMiddleware(req, res, next); - res.once('finish', () => { - span.end(); - }); - res.emit('finish'); - - setImmediate(() => { - expect(finishSpan).toHaveBeenCalled(); - expect(finishTransaction).toHaveBeenCalled(); - expect(spanToJSON(span).timestamp).toBeLessThanOrEqual(spanToJSON(transaction).timestamp!); - expect(sentEvent.spans?.length).toEqual(1); - expect(sentEvent.spans?.[0].span_id).toEqual(span.spanContext().spanId); - done(); - }); - }); -}); - -describe('errorHandler()', () => { - beforeEach(() => { - getCurrentScope().clear(); - getIsolationScope().clear(); - - // Ensure we reset a potentially set acs to use the default - const sentry = getMainCarrier().__SENTRY__; - if (sentry) { - sentry.acs = undefined; - } - }); - - const headers = { ears: 'furry', nose: 'wet', tongue: 'spotted', cookie: 'favorite=zukes' }; - const method = 'wagging'; - const protocol = 'mutualsniffing'; - const hostname = 'the.dog.park'; - const path = '/by/the/trees/'; - const queryString = 'chase=me&please=thankyou'; - - const sentryErrorMiddleware = errorHandler(); - - let req: http.IncomingMessage, res: http.ServerResponse, next: () => undefined; - let client: NodeClient; - - function createNoOpSpy() { - const noop = { noop: () => undefined }; // this is wrapped in an object so jest can spy on it - return jest.spyOn(noop, 'noop') as any; - } - - beforeEach(() => { - req = { - headers, - method, - protocol, - hostname, - originalUrl: `${path}?${queryString}`, - } as unknown as http.IncomingMessage; - res = new http.ServerResponse(req); - next = createNoOpSpy(); - }); - - afterEach(() => { - if ('_sessionFlusher' in client) clearInterval((client as any)._sessionFlusher._intervalId); - jest.restoreAllMocks(); - }); - it('when autoSessionTracking is disabled, does not set requestSession status on Crash', done => { - const options = getDefaultNodeClientOptions({ autoSessionTracking: false, release: '3.3' }); - client = new NodeClient(options); - // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised - // by the`requestHandler`) - client.initSessionFlusher(); - - setCurrentClient(client); - - jest.spyOn(client, '_captureRequestSession'); - - getIsolationScope().setRequestSession({ status: 'ok' }); - - let isolationScope: Scope; - sentryErrorMiddleware({ name: 'error', message: 'this is an error' }, req, res, () => { - isolationScope = getIsolationScope(); - return next(); - }); - - setImmediate(() => { - expect(isolationScope.getRequestSession()).toEqual({ status: 'ok' }); - done(); - }); - }); - - it('autoSessionTracking is enabled + requestHandler is not used -> does not set requestSession status on Crash', done => { - const options = getDefaultNodeClientOptions({ autoSessionTracking: false, release: '3.3' }); - client = new NodeClient(options); - setCurrentClient(client); - - jest.spyOn(client, '_captureRequestSession'); - - getIsolationScope().setRequestSession({ status: 'ok' }); - - let isolationScope: Scope; - sentryErrorMiddleware({ name: 'error', message: 'this is an error' }, req, res, () => { - isolationScope = getIsolationScope(); - return next(); - }); - - setImmediate(() => { - expect(isolationScope.getRequestSession()).toEqual({ status: 'ok' }); - done(); - }); - }); - - it('when autoSessionTracking is enabled, should set requestSession status to Crashed when an unhandled error occurs within the bounds of a request', () => { - const options = getDefaultNodeClientOptions({ autoSessionTracking: true, release: '1.1' }); - client = new NodeClient(options); - // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised - // by the`requestHandler`) - client.initSessionFlusher(); - - setCurrentClient(client); - - jest.spyOn(client, '_captureRequestSession'); - - withScope(() => { - getIsolationScope().setRequestSession({ status: 'ok' }); - sentryErrorMiddleware({ name: 'error', message: 'this is an error' }, req, res, () => { - expect(getIsolationScope().getRequestSession()).toEqual({ status: 'crashed' }); - }); - }); - }); - - it('when autoSessionTracking is enabled, should not set requestSession status on Crash when it occurs outside the bounds of a request', done => { - const options = getDefaultNodeClientOptions({ autoSessionTracking: true, release: '2.2' }); - client = new NodeClient(options); - // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised - // by the`requestHandler`) - client.initSessionFlusher(); - setCurrentClient(client); - - jest.spyOn(client, '_captureRequestSession'); - - let isolationScope: Scope; - sentryErrorMiddleware({ name: 'error', message: 'this is an error' }, req, res, () => { - isolationScope = getIsolationScope(); - return next(); - }); - - setImmediate(() => { - expect(isolationScope.getRequestSession()).toEqual(undefined); - done(); - }); - }); - - it('stores request in `sdkProcessingMetadata`', done => { - const options = getDefaultNodeClientOptions({}); - client = new NodeClient(options); - setCurrentClient(client); - - let isolationScope: Scope; - sentryErrorMiddleware({ name: 'error', message: 'this is an error' }, req, res, () => { - isolationScope = getIsolationScope(); - return next(); - }); - - setImmediate(() => { - expect(isolationScope.getScopeData().sdkProcessingMetadata.request).toEqual(req); - done(); - }); - }); -}); diff --git a/packages/node/test/helper/error.ts b/packages/node/test/helper/error.ts deleted file mode 100644 index 0228338f036b..000000000000 --- a/packages/node/test/helper/error.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function getError(): Error { - return new Error('mock error'); -} diff --git a/packages/node/test/helper/node-client-options.ts b/packages/node/test/helper/node-client-options.ts deleted file mode 100644 index e9428ea0bb7e..000000000000 --- a/packages/node/test/helper/node-client-options.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createTransport } from '@sentry/core'; -import { resolvedSyncPromise } from '@sentry/utils'; - -import type { NodeClientOptions } from '../../src/types'; - -export function getDefaultNodeClientOptions(options: Partial = {}): NodeClientOptions { - return { - integrations: [], - transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => resolvedSyncPromise({})), - stackParser: () => [], - ...options, - }; -} diff --git a/packages/node-experimental/test/helpers/error.ts b/packages/node/test/helpers/error.ts similarity index 100% rename from packages/node-experimental/test/helpers/error.ts rename to packages/node/test/helpers/error.ts diff --git a/packages/node-experimental/test/helpers/getDefaultNodeClientOptions.ts b/packages/node/test/helpers/getDefaultNodeClientOptions.ts similarity index 100% rename from packages/node-experimental/test/helpers/getDefaultNodeClientOptions.ts rename to packages/node/test/helpers/getDefaultNodeClientOptions.ts diff --git a/packages/node-experimental/test/helpers/mockSdkInit.ts b/packages/node/test/helpers/mockSdkInit.ts similarity index 100% rename from packages/node-experimental/test/helpers/mockSdkInit.ts rename to packages/node/test/helpers/mockSdkInit.ts diff --git a/packages/node/test/index.test.ts b/packages/node/test/index.test.ts deleted file mode 100644 index 8379ea34abf1..000000000000 --- a/packages/node/test/index.test.ts +++ /dev/null @@ -1,526 +0,0 @@ -import { - SDK_VERSION, - getGlobalScope, - getIsolationScope, - getMainCarrier, - initAndBind, - setCurrentClient, - withIsolationScope, -} from '@sentry/core'; -import type { EventHint, Integration } from '@sentry/types'; - -import type { Event } from '../src'; -import { contextLinesIntegration, linkedErrorsIntegration } from '../src'; -import { - NodeClient, - addBreadcrumb, - captureEvent, - captureException, - captureMessage, - getClient, - getCurrentHub, - getCurrentScope, - init, -} from '../src'; -import { setNodeAsyncContextStrategy } from '../src/async'; -import { defaultStackParser, getDefaultIntegrations } from '../src/sdk'; -import { getDefaultNodeClientOptions } from './helper/node-client-options'; - -jest.mock('@sentry/core', () => { - const original = jest.requireActual('@sentry/core'); - return { - ...original, - initAndBind: jest.fn().mockImplementation(original.initAndBind), - }; -}); - -const dsn = 'https://53039209a22b4ec1bcc296a3c9fdecd6@sentry.io/4291'; - -// eslint-disable-next-line no-var -declare var global: any; - -describe('SentryNode', () => { - beforeEach(() => { - jest.clearAllMocks(); - getGlobalScope().clear(); - getIsolationScope().clear(); - getCurrentScope().clear(); - getCurrentScope().setClient(undefined); - - init({ dsn }); - }); - - describe('getContext() / setContext()', () => { - test('store/load extra', async () => { - getCurrentScope().setExtra('abc', { def: [1] }); - - expect(getCurrentScope().getScopeData().extra).toEqual({ - abc: { def: [1] }, - }); - }); - - test('store/load tags', async () => { - getCurrentScope().setTag('abc', 'def'); - expect(getCurrentScope().getScopeData().tags).toEqual({ - abc: 'def', - }); - }); - - test('store/load user', async () => { - getCurrentScope().setUser({ id: 'def' }); - expect(getCurrentScope().getScopeData().user).toEqual({ - id: 'def', - }); - }); - }); - - describe('breadcrumbs', () => { - let sendEventSpy: jest.SpyInstance; - - beforeEach(() => { - sendEventSpy = jest - .spyOn(NodeClient.prototype, 'sendEvent') - .mockImplementation(async () => Promise.resolve({ code: 200 })); - }); - - afterEach(() => { - sendEventSpy.mockRestore(); - }); - - test('record auto breadcrumbs', done => { - const options = getDefaultNodeClientOptions({ - beforeSend: (event: Event) => { - // TODO: It should be 3, but we don't capture a breadcrumb - // for our own captureMessage/captureException calls yet - expect(event.breadcrumbs!).toHaveLength(2); - done(); - return null; - }, - dsn, - stackParser: defaultStackParser, - }); - const client = new NodeClient(options); - setCurrentClient(client); - client.init(); - addBreadcrumb({ message: 'test1' }); - addBreadcrumb({ message: 'test2' }); - captureMessage('event'); - }); - }); - - describe('capture', () => { - let sendEventSpy: jest.SpyInstance; - - beforeEach(() => { - sendEventSpy = jest - .spyOn(NodeClient.prototype, 'sendEvent') - .mockImplementation(async () => Promise.resolve({ code: 200 })); - }); - - afterEach(() => { - sendEventSpy.mockRestore(); - }); - - test('capture an exception', done => { - expect.assertions(6); - const options = getDefaultNodeClientOptions({ - stackParser: defaultStackParser, - beforeSend: (event: Event) => { - expect(event.tags).toEqual({ test: '1' }); - expect(event.exception).not.toBeUndefined(); - expect(event.exception!.values![0]).not.toBeUndefined(); - expect(event.exception!.values![0].stacktrace!).not.toBeUndefined(); - expect(event.exception!.values![0].stacktrace!.frames![2]).not.toBeUndefined(); - expect(event.exception!.values![0].value).toEqual('test'); - done(); - return null; - }, - dsn, - }); - const client = new NodeClient(options); - setCurrentClient(client); - client.init(); - getCurrentScope().setTag('test', '1'); - try { - throw new Error('test'); - } catch (e) { - captureException(e); - } - }); - - test('capture a string exception', done => { - expect.assertions(6); - const options = getDefaultNodeClientOptions({ - stackParser: defaultStackParser, - beforeSend: (event: Event) => { - expect(event.tags).toEqual({ test: '1' }); - expect(event.exception).not.toBeUndefined(); - expect(event.exception!.values![0]).not.toBeUndefined(); - expect(event.exception!.values![0].stacktrace!).not.toBeUndefined(); - expect(event.exception!.values![0].stacktrace!.frames![2]).not.toBeUndefined(); - expect(event.exception!.values![0].value).toEqual('test string exception'); - done(); - return null; - }, - dsn, - }); - const client = new NodeClient(options); - setCurrentClient(client); - client.init(); - getCurrentScope().setTag('test', '1'); - try { - throw 'test string exception'; - } catch (e) { - captureException(e); - } - }); - - test('capture an exception with pre/post context', async () => { - const beforeSend = jest.fn((event: Event) => { - expect(event.tags).toEqual({ test: '1' }); - expect(event.exception).not.toBeUndefined(); - expect(event.exception!.values![0]).not.toBeUndefined(); - expect(event.exception!.values![0].stacktrace!).not.toBeUndefined(); - expect(event.exception!.values![0].stacktrace!.frames![1]).not.toBeUndefined(); - expect(event.exception!.values![0].stacktrace!.frames![1].pre_context).not.toBeUndefined(); - expect(event.exception!.values![0].stacktrace!.frames![1].post_context).not.toBeUndefined(); - expect(event.exception!.values![0].type).toBe('Error'); - expect(event.exception!.values![0].value).toBe('test'); - expect(event.exception!.values![0].stacktrace).toBeTruthy(); - return null; - }); - - const options = getDefaultNodeClientOptions({ - stackParser: defaultStackParser, - beforeSend, - dsn, - integrations: [contextLinesIntegration()], - }); - const client = new NodeClient(options); - setCurrentClient(client); - client.init(); - getCurrentScope().setTag('test', '1'); - try { - throw new Error('test'); - } catch (e) { - captureException(e); - } - - await client.flush(); - - expect(beforeSend).toHaveBeenCalledTimes(1); - }); - - test('capture a linked exception with pre/post context', done => { - expect.assertions(15); - const options = getDefaultNodeClientOptions({ - stackParser: defaultStackParser, - integrations: [contextLinesIntegration(), linkedErrorsIntegration()], - beforeSend: (event: Event) => { - expect(event.exception).not.toBeUndefined(); - expect(event.exception!.values![1]).not.toBeUndefined(); - expect(event.exception!.values![1].stacktrace!).not.toBeUndefined(); - expect(event.exception!.values![1].stacktrace!.frames![1]).not.toBeUndefined(); - expect(event.exception!.values![1].stacktrace!.frames![1].pre_context).not.toBeUndefined(); - expect(event.exception!.values![1].stacktrace!.frames![1].post_context).not.toBeUndefined(); - expect(event.exception!.values![1].type).toBe('Error'); - expect(event.exception!.values![1].value).toBe('test'); - - expect(event.exception!.values![0]).not.toBeUndefined(); - expect(event.exception!.values![0].stacktrace!).not.toBeUndefined(); - expect(event.exception!.values![0].stacktrace!.frames![1]).not.toBeUndefined(); - expect(event.exception!.values![0].stacktrace!.frames![1].pre_context).not.toBeUndefined(); - expect(event.exception!.values![0].stacktrace!.frames![1].post_context).not.toBeUndefined(); - expect(event.exception!.values![0].type).toBe('Error'); - expect(event.exception!.values![0].value).toBe('cause'); - done(); - return null; - }, - dsn, - }); - const client = new NodeClient(options); - setCurrentClient(client); - client.init(); - try { - throw new Error('test'); - } catch (e) { - try { - throw new Error('cause'); - } catch (c) { - (e as any).cause = c; - captureException(e); - } - } - }); - - test('capture a message', done => { - expect.assertions(2); - const options = getDefaultNodeClientOptions({ - stackParser: defaultStackParser, - beforeSend: (event: Event) => { - expect(event.message).toBe('test'); - expect(event.exception).toBeUndefined(); - done(); - return null; - }, - dsn, - }); - const client = new NodeClient(options); - setCurrentClient(client); - client.init(); - captureMessage('test'); - }); - - test('capture an event', done => { - expect.assertions(2); - const options = getDefaultNodeClientOptions({ - stackParser: defaultStackParser, - beforeSend: (event: Event) => { - expect(event.message).toBe('test event'); - expect(event.exception).toBeUndefined(); - done(); - return null; - }, - dsn, - }); - const client = new NodeClient(options); - setCurrentClient(client); - client.init(); - captureEvent({ message: 'test event' }); - }); - - test('capture an event in a domain', done => { - const options = getDefaultNodeClientOptions({ - stackParser: defaultStackParser, - beforeSend: (event: Event) => { - expect(event.message).toBe('test domain'); - expect(event.exception).toBeUndefined(); - done(); - return null; - }, - dsn, - }); - setNodeAsyncContextStrategy(); - const client = new NodeClient(options); - - withIsolationScope(() => { - // eslint-disable-next-line deprecation/deprecation - const hub = getCurrentHub(); - setCurrentClient(client); - client.init(); - - // eslint-disable-next-line deprecation/deprecation - expect(getCurrentHub().getClient()).toBe(client); - expect(getClient()).toBe(client); - // eslint-disable-next-line deprecation/deprecation - hub.captureEvent({ message: 'test domain' }); - }); - }); - - test('stacktrace order', done => { - expect.assertions(1); - const options = getDefaultNodeClientOptions({ - stackParser: defaultStackParser, - beforeSend: (event: Event) => { - expect( - event.exception!.values![0].stacktrace!.frames![event.exception!.values![0].stacktrace!.frames!.length - 1] - .function, - ).toEqual('testy'); - done(); - return null; - }, - dsn, - }); - const client = new NodeClient(options); - setCurrentClient(client); - client.init(); - try { - // eslint-disable-next-line no-inner-declarations - function testy(): void { - throw new Error('test'); - } - testy(); - } catch (e) { - captureException(e); - } - }); - }); -}); - -function withAutoloadedIntegrations(integrations: Integration[], callback: () => void) { - const carrier = getMainCarrier(); - carrier.__SENTRY__!.integrations = integrations; - callback(); - carrier.__SENTRY__!.integrations = undefined; - delete carrier.__SENTRY__!.integrations; -} - -/** JSDoc */ -class MockIntegration implements Integration { - public name: string; - - public constructor(name: string) { - this.name = name; - } - - public setupOnce(): void { - // noop - } -} - -describe('SentryNode initialization', () => { - beforeEach(() => { - jest.clearAllMocks(); - - getGlobalScope().clear(); - getIsolationScope().clear(); - getCurrentScope().clear(); - getCurrentScope().setClient(undefined); - }); - - test('global.SENTRY_RELEASE is used to set release on initialization if available', () => { - global.SENTRY_RELEASE = { id: 'foobar' }; - init({ dsn }); - expect(getClient()?.getOptions().release).toEqual('foobar'); - // Unsure if this is needed under jest. - global.SENTRY_RELEASE = undefined; - }); - - describe('SDK metadata', () => { - it('should set SDK data when `Sentry.init()` is called', () => { - init({ dsn }); - - const sdkData = getClient()?.getOptions()._metadata?.sdk || {}; - - expect(sdkData.name).toEqual('sentry.javascript.node'); - expect(sdkData.packages?.[0].name).toEqual('npm:@sentry/node'); - expect(sdkData.packages?.[0].version).toEqual(SDK_VERSION); - expect(sdkData.version).toEqual(SDK_VERSION); - }); - - it('should set SDK data when instantiating a client directly', () => { - const options = getDefaultNodeClientOptions({ dsn }); - const client = new NodeClient(options); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const sdkData = (client as any).getOptions()._metadata.sdk; - - expect(sdkData.name).toEqual('sentry.javascript.node'); - expect(sdkData.packages[0].name).toEqual('npm:@sentry/node'); - expect(sdkData.packages[0].version).toEqual(SDK_VERSION); - expect(sdkData.version).toEqual(SDK_VERSION); - }); - - // wrapper packages (like @sentry/aws-serverless) set their SDK data in their `init` methods, which are - // called before the client is instantiated, and we don't want to clobber that data - it("shouldn't overwrite SDK data that's already there", () => { - init({ - dsn, - // this would normally be set by the wrapper SDK in init() - _metadata: { - sdk: { - name: 'sentry.javascript.aws-serverless', - packages: [ - { - name: 'npm:@sentry/aws-serverless', - version: SDK_VERSION, - }, - ], - version: SDK_VERSION, - }, - }, - }); - - const sdkData = getClient()?.getOptions()._metadata?.sdk || {}; - - expect(sdkData.name).toEqual('sentry.javascript.aws-serverless'); - expect(sdkData.packages?.[0].name).toEqual('npm:@sentry/aws-serverless'); - expect(sdkData.packages?.[0].version).toEqual(SDK_VERSION); - expect(sdkData.version).toEqual(SDK_VERSION); - }); - }); - - describe('autoloaded integrations', () => { - it('should attach integrations to default integrations', () => { - withAutoloadedIntegrations([new MockIntegration('foo')], () => { - init({ - defaultIntegrations: [...getDefaultIntegrations({}), new MockIntegration('bar')], - }); - const integrations = (initAndBind as jest.Mock).mock.calls[0][1].defaultIntegrations; - expect(integrations.map((i: { name: string }) => i.name)).toEqual(expect.arrayContaining(['foo', 'bar'])); - }); - }); - - it('should ignore autoloaded integrations when `defaultIntegrations` is `false`', () => { - withAutoloadedIntegrations([new MockIntegration('foo')], () => { - init({ - defaultIntegrations: false, - }); - const integrations = (initAndBind as jest.Mock).mock.calls[0][1].defaultIntegrations; - expect(integrations).toEqual(false); - }); - }); - }); - - describe('autoSessionTracking', () => { - it('enables autoSessionTracking if there is a release', () => { - init({ - dsn: '', - release: '3.5.7', - }); - - const options = (initAndBind as jest.Mock).mock.calls[0][1]; - expect(options.autoSessionTracking).toBe(true); - }); - - it('disables autoSessionTracking if dsn is undefined', () => { - init({ - release: '3.5.7', - }); - - const options = (initAndBind as jest.Mock).mock.calls[0][1]; - expect(options.autoSessionTracking).toBe(undefined); - }); - }); - - describe('propagation context', () => { - beforeEach(() => { - process.env.SENTRY_TRACE = '12312012123120121231201212312012-1121201211212012-0'; - process.env.SENTRY_BAGGAGE = 'sentry-release=1.0.0,sentry-environment=production'; - }); - - afterEach(() => { - delete process.env.SENTRY_TRACE; - delete process.env.SENTRY_BAGGAGE; - }); - - it('reads from environmental variables', () => { - init({ dsn }); - - // @ts-expect-error accessing private method for test - expect(getCurrentScope()._propagationContext).toEqual({ - traceId: '12312012123120121231201212312012', - parentSpanId: '1121201211212012', - spanId: expect.any(String), - sampled: false, - dsc: { - release: '1.0.0', - environment: 'production', - }, - }); - }); - - it.each(['false', 'False', 'FALSE', 'n', 'no', 'No', 'NO', 'off', 'Off', 'OFF', '0'])( - 'does not read from environmental variable if SENTRY_USE_ENVIRONMENT is set to %s', - useEnvValue => { - process.env.SENTRY_USE_ENVIRONMENT = useEnvValue; - init({ dsn }); - - // @ts-expect-error accessing private method for test - expect(getCurrentScope()._propagationContext.traceId).not.toEqual('12312012123120121231201212312012'); - - delete process.env.SENTRY_USE_ENVIRONMENT; - }, - ); - }); -}); diff --git a/packages/node-experimental/test/integration/breadcrumbs.test.ts b/packages/node/test/integration/breadcrumbs.test.ts similarity index 100% rename from packages/node-experimental/test/integration/breadcrumbs.test.ts rename to packages/node/test/integration/breadcrumbs.test.ts diff --git a/packages/node-experimental/test/integration/scope.test.ts b/packages/node/test/integration/scope.test.ts similarity index 100% rename from packages/node-experimental/test/integration/scope.test.ts rename to packages/node/test/integration/scope.test.ts diff --git a/packages/node-experimental/test/integration/transactions.test.ts b/packages/node/test/integration/transactions.test.ts similarity index 100% rename from packages/node-experimental/test/integration/transactions.test.ts rename to packages/node/test/integration/transactions.test.ts diff --git a/packages/node/test/integrations/contextlines.test.ts b/packages/node/test/integrations/contextlines.test.ts index 67f3ad793fbc..c4ef1efaa292 100644 --- a/packages/node/test/integrations/contextlines.test.ts +++ b/packages/node/test/integrations/contextlines.test.ts @@ -1,30 +1,37 @@ -import * as fs from 'fs'; -import type { Event, Integration, StackFrame } from '@sentry/types'; +import { promises } from 'fs'; +import type { StackFrame } from '@sentry/types'; import { parseStackFrames } from '@sentry/utils'; -import { contextLinesIntegration } from '../../src'; -import { resetFileContentCache } from '../../src/integrations/contextlines'; -import { defaultStackParser } from '../../src/sdk'; -import { getError } from '../helper/error'; +import { _contextLinesIntegration, resetFileContentCache } from '../../src/integrations/contextlines'; +import { defaultStackParser } from '../../src/sdk/api'; +import { getError } from '../helpers/error'; + +jest.mock('fs', () => { + const actual = jest.requireActual('fs'); + return { + ...actual, + promises: { + ...actual.promises, + readFile: jest.fn(actual.promises), + }, + }; +}); describe('ContextLines', () => { - let readFileSpy: jest.SpyInstance; - let contextLines: Integration; + const readFileSpy = promises.readFile as unknown as jest.SpyInstance; + let contextLines: ReturnType; async function addContext(frames: StackFrame[]): Promise { - await (contextLines as Integration & { processEvent: (event: Event) => Promise }).processEvent({ - exception: { values: [{ stacktrace: { frames } }] }, - }); + await contextLines.processEvent({ exception: { values: [{ stacktrace: { frames } }] } }); } beforeEach(() => { - readFileSpy = jest.spyOn(fs, 'readFile'); - contextLines = contextLinesIntegration(); + contextLines = _contextLinesIntegration(); resetFileContentCache(); }); afterEach(() => { - jest.restoreAllMocks(); + jest.clearAllMocks(); }); describe('lru file cache', () => { @@ -101,7 +108,7 @@ describe('ContextLines', () => { }); test('parseStack with no context', async () => { - contextLines = contextLinesIntegration({ frameContextLines: 0 }); + contextLines = _contextLinesIntegration({ frameContextLines: 0 }); expect.assertions(1); const frames = parseStackFrames(defaultStackParser, new Error('test')); @@ -113,7 +120,6 @@ describe('ContextLines', () => { test('does not attempt to readfile multiple times if it fails', async () => { expect.assertions(1); - contextLines = contextLinesIntegration(); readFileSpy.mockImplementation(() => { throw new Error("ENOENT: no such file or directory, open '/does/not/exist.js'"); diff --git a/packages/node-experimental/test/integrations/express.test.ts b/packages/node/test/integrations/express.test.ts similarity index 100% rename from packages/node-experimental/test/integrations/express.test.ts rename to packages/node/test/integrations/express.test.ts diff --git a/packages/node/test/integrations/http.test.ts b/packages/node/test/integrations/http.test.ts deleted file mode 100644 index df206904b48f..000000000000 --- a/packages/node/test/integrations/http.test.ts +++ /dev/null @@ -1,777 +0,0 @@ -import * as http from 'http'; -import * as https from 'https'; -import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, getSpanDescendants, startSpan } from '@sentry/core'; -import { getCurrentHub, getIsolationScope, setCurrentClient } from '@sentry/core'; -import { Transaction } from '@sentry/core'; -import { getCurrentScope, setUser, spanToJSON, startInactiveSpan } from '@sentry/core'; -import { addTracingExtensions } from '@sentry/core'; -import type { TransactionContext } from '@sentry/types'; -import { TRACEPARENT_REGEXP } from '@sentry/utils'; -import * as nock from 'nock'; -import { HttpsProxyAgent } from '../../src/proxy'; - -import type { Breadcrumb } from '../../src'; -import { _setSpanForScope } from '../../src/_setSpanForScope'; -import { NodeClient } from '../../src/client'; -import { - Http as HttpIntegration, - _getShouldCreateSpanForRequest, - _shouldCreateSpans, - httpIntegration, -} from '../../src/integrations/http'; -import { NODE_VERSION } from '../../src/nodeVersion'; -import type { NodeClientOptions } from '../../src/types'; -import { getDefaultNodeClientOptions } from '../helper/node-client-options'; - -const originalHttpGet = http.get; -const originalHttpRequest = http.request; - -describe('tracing', () => { - beforeEach(() => { - getCurrentScope().clear(); - getIsolationScope().clear(); - }); - - afterEach(() => { - _setSpanForScope(getCurrentScope(), undefined); - }); - - function createTransactionOnScope( - customOptions: Partial = {}, - customContext?: Partial, - ) { - setupMockHub(customOptions); - addTracingExtensions(); - - setUser({ - id: 'uid123', - }); - - const transaction = startInactiveSpan({ - name: 'dogpark', - traceId: '12312012123120121231201212312012', - ...customContext, - }); - - expect(transaction).toBeInstanceOf(Transaction); - _setSpanForScope(getCurrentScope(), transaction); - return transaction; - } - - function setupMockHub(customOptions: Partial = {}) { - const options = getDefaultNodeClientOptions({ - dsn: 'https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.sentry.io/12312012', - tracesSampleRate: 1.0, - // eslint-disable-next-line deprecation/deprecation - integrations: [new HttpIntegration({ tracing: true })], - release: '1.0.0', - environment: 'production', - ...customOptions, - }); - const client = new NodeClient(options); - setCurrentClient(client); - client.init(); - } - - it("creates a span for each outgoing non-sentry request when there's a transaction on the scope", () => { - nock('http://dogs.are.great').get('/').reply(200); - - const transaction = createTransactionOnScope(); - - http.get('http://dogs.are.great/'); - - const spans = getSpanDescendants(transaction); - - // our span is at index 1 because the transaction itself is at index 0 - expect(spanToJSON(spans[1]).description).toEqual('GET http://dogs.are.great/'); - expect(spanToJSON(spans[1]).op).toEqual('http.client'); - }); - - it("doesn't create a span for outgoing sentry requests", () => { - nock('http://squirrelchasers.ingest.sentry.io').get('/api/12312012/store/').reply(200); - - const transaction = createTransactionOnScope(); - - http.get('http://squirrelchasers.ingest.sentry.io/api/12312012/store/'); - - const spans = getSpanDescendants(transaction); - - // only the transaction itself should be there - expect(spans.length).toEqual(1); - expect(spanToJSON(spans[0]).description).toEqual('dogpark'); - }); - - it('attaches the sentry-trace header to outgoing non-sentry requests', async () => { - nock('http://dogs.are.great').get('/').reply(200); - - createTransactionOnScope(); - - const request = http.get('http://dogs.are.great/'); - const sentryTraceHeader = request.getHeader('sentry-trace') as string; - - expect(sentryTraceHeader).toBeDefined(); - expect(TRACEPARENT_REGEXP.test(sentryTraceHeader)).toBe(true); - }); - - it("doesn't attach the sentry-trace header to outgoing sentry requests", () => { - nock('http://squirrelchasers.ingest.sentry.io').get('/api/12312012/store/').reply(200); - - createTransactionOnScope(); - - const request = http.get('http://squirrelchasers.ingest.sentry.io/api/12312012/store/'); - const sentryTraceHeader = request.getHeader('sentry-trace'); - - expect(sentryTraceHeader).not.toBeDefined(); - }); - - it('attaches the baggage header to outgoing non-sentry requests', async () => { - nock('http://dogs.are.great').get('/').reply(200); - - createTransactionOnScope(); - - const request = http.get('http://dogs.are.great/'); - const baggageHeader = request.getHeader('baggage') as string; - - expect(baggageHeader).toEqual( - 'sentry-environment=production,sentry-release=1.0.0,' + - 'sentry-public_key=dogsarebadatkeepingsecrets,' + - 'sentry-trace_id=12312012123120121231201212312012,sentry-sample_rate=1,' + - 'sentry-transaction=dogpark,sentry-sampled=true', - ); - }); - - it('keeps 3rd party baggage header data to outgoing non-sentry requests', async () => { - nock('http://dogs.are.great').get('/').reply(200); - - createTransactionOnScope(); - - const request = http.get({ host: 'http://dogs.are.great/', headers: { baggage: 'dog=great' } }); - const baggageHeader = request.getHeader('baggage') as string; - - expect(baggageHeader[0]).toEqual('dog=great'); - expect(baggageHeader[1]).toEqual( - 'sentry-environment=production,sentry-release=1.0.0,sentry-public_key=dogsarebadatkeepingsecrets,sentry-trace_id=12312012123120121231201212312012,sentry-sample_rate=1,sentry-transaction=dogpark,sentry-sampled=true', - ); - }); - - it('adds the transaction name to the the baggage header if a valid transaction source is set', async () => { - nock('http://dogs.are.great').get('/').reply(200); - - createTransactionOnScope({}, { attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route' } }); - - const request = http.get({ host: 'http://dogs.are.great/', headers: { baggage: 'dog=great' } }); - const baggageHeader = request.getHeader('baggage') as string; - - expect(baggageHeader).toEqual([ - 'dog=great', - 'sentry-environment=production,sentry-release=1.0.0,sentry-public_key=dogsarebadatkeepingsecrets,sentry-trace_id=12312012123120121231201212312012,sentry-sample_rate=1,sentry-transaction=dogpark,sentry-sampled=true', - ]); - }); - - it('does not add the transaction name to the the baggage header if url transaction source is set', async () => { - nock('http://dogs.are.great').get('/').reply(200); - - createTransactionOnScope({}, { attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' } }); - - const request = http.get({ host: 'http://dogs.are.great/', headers: { baggage: 'dog=great' } }); - const baggageHeader = request.getHeader('baggage') as string; - - expect(baggageHeader).toEqual([ - 'dog=great', - 'sentry-environment=production,sentry-release=1.0.0,sentry-public_key=dogsarebadatkeepingsecrets,sentry-trace_id=12312012123120121231201212312012,sentry-sample_rate=1,sentry-sampled=true', - ]); - }); - - it("doesn't attach baggage headers if already defined", () => { - nock('http://dogs.are.great').get('/').reply(200); - - createTransactionOnScope(); - - const request = http.get({ - host: 'http://dogs.are.great/', - headers: { - 'sentry-trace': '12312012123120121231201212312012-1231201212312012-0', - baggage: 'sentry-environment=production,sentry-trace_id=12312012123120121231201212312012', - }, - }); - const baggage = request.getHeader('baggage'); - expect(baggage).toEqual('sentry-environment=production,sentry-trace_id=12312012123120121231201212312012'); - }); - - it('generates and uses propagation context to attach baggage and sentry-trace header', async () => { - nock('http://dogs.are.great').get('/').reply(200); - - const { traceId } = getCurrentScope().getPropagationContext(); - - // Needs an outer span, or else we skip it - const request = startSpan({ name: 'outer' }, () => http.get('http://dogs.are.great/')); - const sentryTraceHeader = request.getHeader('sentry-trace') as string; - const baggageHeader = request.getHeader('baggage') as string; - - const parts = sentryTraceHeader.split('-'); - - expect(parts.length).toEqual(3); - expect(parts[0]).toEqual(traceId); - expect(parts[1]).toEqual(expect.any(String)); - expect(parts[2]).toEqual('1'); - - expect(baggageHeader).toEqual( - `sentry-environment=production,sentry-release=1.0.0,sentry-public_key=dogsarebadatkeepingsecrets,sentry-trace_id=${traceId},sentry-sample_rate=1,sentry-transaction=outer,sentry-sampled=true`, - ); - }); - - it('uses incoming propagation context to attach baggage and sentry-trace', async () => { - nock('http://dogs.are.great').get('/').reply(200); - - setupMockHub(); - getCurrentScope().setPropagationContext({ - traceId: '86f39e84263a4de99c326acab3bfe3bd', - spanId: '86f39e84263a4de9', - sampled: true, - dsc: { - trace_id: '86f39e84263a4de99c326acab3bfe3bd', - public_key: 'test-public-key', - }, - }); - - // Needs an outer span, or else we skip it - const request = startSpan({ name: 'outer' }, () => http.get('http://dogs.are.great/')); - const sentryTraceHeader = request.getHeader('sentry-trace') as string; - const baggageHeader = request.getHeader('baggage') as string; - - const parts = sentryTraceHeader.split('-'); - expect(parts).toEqual(['86f39e84263a4de99c326acab3bfe3bd', expect.any(String), '1']); - expect(baggageHeader).toEqual('sentry-trace_id=86f39e84263a4de99c326acab3bfe3bd,sentry-public_key=test-public-key'); - }); - - it("doesn't attach the sentry-trace header to outgoing sentry requests", () => { - nock('http://squirrelchasers.ingest.sentry.io').get('/api/12312012/store/').reply(200); - - createTransactionOnScope(); - - const request = http.get('http://squirrelchasers.ingest.sentry.io/api/12312012/store/'); - const baggage = request.getHeader('baggage'); - - expect(baggage).not.toBeDefined(); - }); - - it('omits query and fragment from description and adds to span data instead', () => { - nock('http://dogs.are.great').get('/spaniel?tail=wag&cute=true#learn-more').reply(200); - - const transaction = createTransactionOnScope(); - - http.get('http://dogs.are.great/spaniel?tail=wag&cute=true#learn-more'); - - const spans = getSpanDescendants(transaction); - - expect(spans.length).toEqual(2); - - // our span is at index 1 because the transaction itself is at index 0 - expect(spanToJSON(spans[1]).description).toEqual('GET http://dogs.are.great/spaniel'); - expect(spanToJSON(spans[1]).op).toEqual('http.client'); - - const spanAttributes = spanToJSON(spans[1]).data || {}; - - expect(spanAttributes['http.method']).toEqual('GET'); - expect(spanAttributes.url).toEqual('http://dogs.are.great/spaniel'); - expect(spanAttributes['http.query']).toEqual('tail=wag&cute=true'); - expect(spanAttributes['http.fragment']).toEqual('learn-more'); - }); - - it('fills in span data from http.RequestOptions object', () => { - nock('http://dogs.are.great').get('/spaniel?tail=wag&cute=true#learn-more').reply(200); - - const transaction = createTransactionOnScope(); - - http.request({ method: 'GET', host: 'dogs.are.great', path: '/spaniel?tail=wag&cute=true#learn-more' }); - - const spans = getSpanDescendants(transaction); - - expect(spans.length).toEqual(2); - - const spanAttributes = spanToJSON(spans[1]).data || {}; - - // our span is at index 1 because the transaction itself is at index 0 - expect(spanToJSON(spans[1]).description).toEqual('GET http://dogs.are.great/spaniel'); - expect(spanToJSON(spans[1]).op).toEqual('http.client'); - expect(spanAttributes['http.method']).toEqual('GET'); - expect(spanAttributes.url).toEqual('http://dogs.are.great/spaniel'); - expect(spanAttributes['http.query']).toEqual('tail=wag&cute=true'); - expect(spanAttributes['http.fragment']).toEqual('learn-more'); - }); - - it.each([ - ['user:pwd', '[Filtered]:[Filtered]@'], - ['user:', '[Filtered]:@'], - ['user', '[Filtered]:@'], - [':pwd', ':[Filtered]@'], - ['', ''], - ])('filters the authority %s in span description', (auth, redactedAuth) => { - nock(`http://${auth}@dogs.are.great`).get('/').reply(200); - - const transaction = createTransactionOnScope(); - - http.get(`http://${auth}@dogs.are.great/`); - - const spans = getSpanDescendants(transaction); - - expect(spans.length).toEqual(2); - - // our span is at index 1 because the transaction itself is at index 0 - expect(spanToJSON(spans[1]).description).toEqual(`GET http://${redactedAuth}dogs.are.great/`); - }); - - describe('Tracing options', () => { - beforeEach(() => { - // hacky way of restoring monkey patched functions - // @ts-expect-error TS doesn't let us assign to this but we want to - http.get = originalHttpGet; - // @ts-expect-error TS doesn't let us assign to this but we want to - http.request = originalHttpRequest; - }); - - function createHub(customOptions: Partial = {}) { - const options = getDefaultNodeClientOptions({ - dsn: 'https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.sentry.io/12312012', - tracesSampleRate: 1.0, - release: '1.0.0', - environment: 'production', - ...customOptions, - }); - - const client = new NodeClient(options); - setCurrentClient(client); - client.init(); - // eslint-disable-next-line deprecation/deprecation - const hub = getCurrentHub(); - - return hub; - } - - function createTransactionAndPutOnScope() { - addTracingExtensions(); - const transaction = startInactiveSpan({ name: 'dogpark' }); - _setSpanForScope(getCurrentScope(), transaction); - return transaction; - } - - describe('as client options', () => { - it('creates span with propagation context if shouldCreateSpanForRequest returns false', () => { - const url = 'http://dogs.are.great/api/v1/index/'; - nock(url).get(/.*/).reply(200); - - // eslint-disable-next-line deprecation/deprecation - const httpIntegration = new HttpIntegration({ tracing: true }); - - createHub({ shouldCreateSpanForRequest: () => false }); - - httpIntegration.setupOnce(); - - const transaction = createTransactionAndPutOnScope(); - - const request = http.get(url); - - const spans = getSpanDescendants(transaction); - - // There should be no http spans - const httpSpans = spans.filter(span => spanToJSON(span).op?.startsWith('http')); - expect(httpSpans.length).toBe(0); - - // And headers are not attached without span creation - expect(request.getHeader('sentry-trace')).toBeDefined(); - expect(request.getHeader('baggage')).toBeDefined(); - - const propagationContext = getCurrentScope().getPropagationContext(); - - expect((request.getHeader('sentry-trace') as string).includes(propagationContext.traceId)).toBe(true); - expect(request.getHeader('baggage')).toEqual( - `sentry-environment=production,sentry-release=1.0.0,sentry-public_key=dogsarebadatkeepingsecrets,sentry-trace_id=${propagationContext.traceId}`, - ); - }); - - it.each([ - ['http://dogs.are.great/api/v1/index/', [/.*/]], - ['http://dogs.are.great/api/v1/index/', [/\/api/]], - ['http://dogs.are.great/api/v1/index/', [/\/(v1|v2)/]], - ['http://dogs.are.great/api/v1/index/', [/dogs\.are\.great/, /dogs\.are\.not\.great/]], - ['http://dogs.are.great/api/v1/index/', [/http:/]], - ['http://dogs.are.great/api/v1/index/', ['dogs.are.great']], - ['http://dogs.are.great/api/v1/index/', ['/api/v1']], - ['http://dogs.are.great/api/v1/index/', ['http://']], - ['http://dogs.are.great/api/v1/index/', ['']], - ])( - 'attaches trace inforation to header of outgoing requests when url matches tracePropagationTargets (url="%s", tracePropagationTargets=%p)', - (url, tracePropagationTargets) => { - nock(url).get(/.*/).reply(200); - - // eslint-disable-next-line deprecation/deprecation - const httpIntegration = new HttpIntegration({ tracing: true }); - - createHub({ tracePropagationTargets }); - - httpIntegration.setupOnce(); - - createTransactionAndPutOnScope(); - - const request = http.get(url); - - expect(request.getHeader('sentry-trace')).toBeDefined(); - expect(request.getHeader('baggage')).toBeDefined(); - }, - ); - - it.each([ - ['http://dogs.are.great/api/v1/index/', []], - ['http://cats.are.great/api/v1/index/', [/\/v2/]], - ['http://cats.are.great/api/v1/index/', [/\/(v2|v3)/]], - ['http://cats.are.great/api/v1/index/', [/dogs\.are\.great/, /dogs\.are\.not\.great/]], - ['http://cats.are.great/api/v1/index/', [/https:/]], - ['http://cats.are.great/api/v1/index/', ['dogs.are.great']], - ['http://cats.are.great/api/v1/index/', ['/api/v2']], - ['http://cats.are.great/api/v1/index/', ['https://']], - ])( - 'doesn\'t attach trace inforation to header of outgoing requests when url doesn\'t match tracePropagationTargets (url="%s", tracePropagationTargets=%p)', - (url, tracePropagationTargets) => { - nock(url).get(/.*/).reply(200); - - // eslint-disable-next-line deprecation/deprecation - const httpIntegration = new HttpIntegration({ tracing: true }); - - createHub({ tracePropagationTargets }); - - httpIntegration.setupOnce(); - - createTransactionAndPutOnScope(); - - const request = http.get(url); - - expect(request.getHeader('sentry-trace')).not.toBeDefined(); - expect(request.getHeader('baggage')).not.toBeDefined(); - }, - ); - }); - - describe('as Http integration constructor options', () => { - it('creates span with propagation context if shouldCreateSpanForRequest returns false', () => { - const url = 'http://dogs.are.great/api/v1/index/'; - nock(url).get(/.*/).reply(200); - - // eslint-disable-next-line deprecation/deprecation - const httpIntegration = new HttpIntegration({ - tracing: { - shouldCreateSpanForRequest: () => false, - }, - }); - - createHub(); - - httpIntegration.setupOnce(); - - const transaction = createTransactionAndPutOnScope(); - - const request = http.get(url); - - const spans = getSpanDescendants(transaction); - - // There should be no http spans - const httpSpans = spans.filter(span => spanToJSON(span).op?.startsWith('http')); - expect(httpSpans.length).toBe(0); - - // And headers are not attached without span creation - expect(request.getHeader('sentry-trace')).toBeDefined(); - expect(request.getHeader('baggage')).toBeDefined(); - - const propagationContext = getCurrentScope().getPropagationContext(); - - expect((request.getHeader('sentry-trace') as string).includes(propagationContext.traceId)).toBe(true); - expect(request.getHeader('baggage')).toEqual( - `sentry-environment=production,sentry-release=1.0.0,sentry-public_key=dogsarebadatkeepingsecrets,sentry-trace_id=${propagationContext.traceId}`, - ); - }); - - it.each([ - ['http://dogs.are.great/api/v1/index/', [/.*/]], - ['http://dogs.are.great/api/v1/index/', [/\/api/]], - ['http://dogs.are.great/api/v1/index/', [/\/(v1|v2)/]], - ['http://dogs.are.great/api/v1/index/', [/dogs\.are\.great/, /dogs\.are\.not\.great/]], - ['http://dogs.are.great/api/v1/index/', [/http:/]], - ['http://dogs.are.great/api/v1/index/', ['dogs.are.great']], - ['http://dogs.are.great/api/v1/index/', ['/api/v1']], - ['http://dogs.are.great/api/v1/index/', ['http://']], - ['http://dogs.are.great/api/v1/index/', ['']], - ])( - 'attaches trace inforation to header of outgoing requests when url matches tracePropagationTargets (url="%s", tracePropagationTargets=%p)', - (url, tracePropagationTargets) => { - nock(url).get(/.*/).reply(200); - - // eslint-disable-next-line deprecation/deprecation - const httpIntegration = new HttpIntegration({ tracing: { tracePropagationTargets } }); - - createHub(); - - httpIntegration.setupOnce(); - - createTransactionAndPutOnScope(); - - const request = http.get(url); - - expect(request.getHeader('sentry-trace')).toBeDefined(); - expect(request.getHeader('baggage')).toBeDefined(); - }, - ); - - it.each([ - ['http://dogs.are.great/api/v1/index/', []], - ['http://cats.are.great/api/v1/index/', [/\/v2/]], - ['http://cats.are.great/api/v1/index/', [/\/(v2|v3)/]], - ['http://cats.are.great/api/v1/index/', [/dogs\.are\.great/, /dogs\.are\.not\.great/]], - ['http://cats.are.great/api/v1/index/', [/https:/]], - ['http://cats.are.great/api/v1/index/', ['dogs.are.great']], - ['http://cats.are.great/api/v1/index/', ['/api/v2']], - ['http://cats.are.great/api/v1/index/', ['https://']], - ])( - 'doesn\'t attach trace inforation to header of outgoing requests when url doesn\'t match tracePropagationTargets (url="%s", tracePropagationTargets=%p)', - (url, tracePropagationTargets) => { - nock(url).get(/.*/).reply(200); - - // eslint-disable-next-line deprecation/deprecation - const httpIntegration = new HttpIntegration({ tracing: { tracePropagationTargets } }); - - createHub(); - - httpIntegration.setupOnce(); - - createTransactionAndPutOnScope(); - - const request = http.get(url); - - expect(request.getHeader('sentry-trace')).not.toBeDefined(); - expect(request.getHeader('baggage')).not.toBeDefined(); - }, - ); - }); - }); -}); - -describe('default protocols', () => { - function captureBreadcrumb(key: string): Promise { - let resolve: (value: Breadcrumb | PromiseLike) => void; - const p = new Promise(r => { - resolve = r; - }); - const options = getDefaultNodeClientOptions({ - dsn: 'https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.sentry.io/12312012', - // eslint-disable-next-line deprecation/deprecation - integrations: [new HttpIntegration({ breadcrumbs: true })], - beforeBreadcrumb: (b: Breadcrumb) => { - if ((b.data?.url as string).includes(key)) { - resolve(b); - } - return b; - }, - }); - const client = new NodeClient(options); - setCurrentClient(client); - client.init(); - - return p; - } - - it('http module', async () => { - const key = 'catrunners'; - const p = captureBreadcrumb(key); - - nock(`http://${key}.ingest.sentry.io`).get('/api/123122332/store/').reply(200); - - http.get({ - host: `${key}.ingest.sentry.io`, - path: '/api/123122332/store/', - }); - - const b = await p; - expect(b.data?.url).toEqual(expect.stringContaining('http://')); - }); - - it('https module', async () => { - const key = 'catcatchers'; - const p = captureBreadcrumb(key); - - let nockProtocol = 'https'; - // NOTE: Prior to Node 9, `https` used internals of `http` module, so - // the integration doesn't patch the `https` module. However this then - // causes issues with nock, because nock will patch the `https` module - // regardless (if it asked to mock a https:// url), preventing the - // request from reaching the integrations patch of the `http` module. - // The result is test failures in Node v8 and lower. - // - // We can work around this by telling giving nock a http:// url, so it - // only patches the `http` module, then Nodes usage of the `http` module - // in the `https` module results in both nock's and the integrations - // patch being called. All this relies on nock not properly checking - // the agent passed to `http.get` / `http.request`, thus resulting in it - // intercepting a https:// request with http:// mock. It's a safe bet - // because the latest versions of nock no longer support Node v8 and lower, - // so won't bother dealing with this old Node edge case. - if (NODE_VERSION.major < 9) { - nockProtocol = 'http'; - } - nock(`${nockProtocol}://${key}.ingest.sentry.io`).get('/api/123122332/store/').reply(200); - - https.get({ - host: `${key}.ingest.sentry.io`, - path: '/api/123122332/store/', - timeout: 300, - }); - - const b = await p; - expect(b.data?.url).toEqual(expect.stringContaining('https://')); - }); - - it('makes https request over http proxy', async () => { - const key = 'catcatchers'; - const p = captureBreadcrumb(key); - - const proxy = 'http://some.url:3128'; - const agent = new HttpsProxyAgent(proxy); - - nock(`https://${key}.ingest.sentry.io`).get('/api/123122332/store/').reply(200); - - https.get({ - host: `${key}.ingest.sentry.io`, - path: '/api/123122332/store/', - timeout: 300, - agent, - }); - - const b = await p; - expect(b.data?.url).toEqual(expect.stringContaining('https://')); - }); -}); - -describe('httpIntegration', () => { - beforeEach(function () { - const options = getDefaultNodeClientOptions({ - dsn: 'https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.sentry.io/12312012', - tracesSampleRate: 1.0, - release: '1.0.0', - environment: 'production', - }); - const client = new NodeClient(options); - setCurrentClient(client); - client.init(); - }); - - it('converts default options', () => { - // eslint-disable-next-line deprecation/deprecation - const integration = httpIntegration({}) as unknown as HttpIntegration; - - expect(integration['_breadcrumbs']).toBe(true); - expect(integration['_tracing']).toEqual({ enableIfHasTracingEnabled: true }); - }); - - it('respects `tracing=false`', () => { - // eslint-disable-next-line deprecation/deprecation - const integration = httpIntegration({ tracing: false }) as unknown as HttpIntegration; - - expect(integration['_tracing']).toEqual(undefined); - }); - - it('respects `breadcrumbs=false`', () => { - // eslint-disable-next-line deprecation/deprecation - const integration = httpIntegration({ breadcrumbs: false }) as unknown as HttpIntegration; - - expect(integration['_breadcrumbs']).toBe(false); - }); - - it('respects `tracing=true`', () => { - // eslint-disable-next-line deprecation/deprecation - const integration = httpIntegration({ tracing: true }) as unknown as HttpIntegration; - - expect(integration['_tracing']).toEqual({}); - }); - - it('respects `shouldCreateSpanForRequest`', () => { - const shouldCreateSpanForRequest = jest.fn(); - - // eslint-disable-next-line deprecation/deprecation - const integration = httpIntegration({ shouldCreateSpanForRequest }) as unknown as HttpIntegration; - - expect(integration['_tracing']).toEqual({ - shouldCreateSpanForRequest, - enableIfHasTracingEnabled: true, - }); - }); - - it('respects `shouldCreateSpanForRequest` & `tracing=true`', () => { - const shouldCreateSpanForRequest = jest.fn(); - - // eslint-disable-next-line deprecation/deprecation - const integration = httpIntegration({ shouldCreateSpanForRequest, tracing: true }) as unknown as HttpIntegration; - - expect(integration['_tracing']).toEqual({ - shouldCreateSpanForRequest, - }); - }); -}); - -describe('_shouldCreateSpans', () => { - beforeEach(function () { - getCurrentScope().clear(); - getIsolationScope().clear(); - }); - - it.each([ - [undefined, undefined, false], - [{}, undefined, true], - [{ enableIfHasTracingEnabled: true }, undefined, false], - [{ enableIfHasTracingEnabled: false }, undefined, true], - [{ enableIfHasTracingEnabled: true }, { tracesSampleRate: 1 }, true], - [{ enableIfHasTracingEnabled: true }, { tracesSampleRate: 0 }, true], - [{}, {}, true], - ])('works with tracing=%p and clientOptions=%p', (tracing, clientOptions, expected) => { - const client = new NodeClient(getDefaultNodeClientOptions(clientOptions)); - setCurrentClient(client); - const actual = _shouldCreateSpans(tracing, clientOptions); - expect(actual).toEqual(expected); - }); -}); - -describe('_getShouldCreateSpanForRequest', () => { - beforeEach(function () { - getCurrentScope().clear(); - getIsolationScope().clear(); - }); - - it.each([ - [false, undefined, undefined, { a: false, b: false }], - [true, undefined, undefined, undefined], - // with tracing callback only - [true, { shouldCreateSpanForRequest: (url: string) => url === 'a' }, undefined, { a: true, b: false }], - // with client callback only - [true, undefined, { shouldCreateSpanForRequest: (url: string) => url === 'a' }, { a: true, b: false }], - // with both callbacks, tracing takes precedence - [ - true, - { shouldCreateSpanForRequest: (url: string) => url === 'a' }, - { shouldCreateSpanForRequest: (url: string) => url === 'b' }, - { a: true, b: false }, - ], - // If `shouldCreateSpans===false`, the callback is ignored - [false, { shouldCreateSpanForRequest: (url: string) => url === 'a' }, undefined, { a: false, b: false }], - ])( - 'works with shouldCreateSpans=%p, tracing=%p and clientOptions=%p', - (shouldCreateSpans, tracing, clientOptions, expected) => { - const actual = _getShouldCreateSpanForRequest(shouldCreateSpans, tracing, clientOptions); - - if (typeof expected === 'object') { - expect(typeof actual).toBe('function'); - - for (const [url, shouldBe] of Object.entries(expected)) { - expect(actual!(url)).toEqual(shouldBe); - } - } else { - expect(actual).toEqual(expected); - } - }, - ); -}); diff --git a/packages/node/test/integrations/localvariables.test.ts b/packages/node/test/integrations/localvariables.test.ts index abc1d241f842..db9385214d42 100644 --- a/packages/node/test/integrations/localvariables.test.ts +++ b/packages/node/test/integrations/localvariables.test.ts @@ -1,12 +1,12 @@ import { createRateLimiter } from '../../src/integrations/local-variables/common'; import { createCallbackList } from '../../src/integrations/local-variables/local-variables-sync'; -import { NODE_VERSION } from '../../src/nodeVersion'; +import { NODE_MAJOR } from '../../src/nodeVersion'; jest.setTimeout(20_000); const describeIf = (condition: boolean) => (condition ? describe : describe.skip); -describeIf(NODE_VERSION.major >= 18)('LocalVariables', () => { +describeIf(NODE_MAJOR >= 18)('LocalVariables', () => { describe('createCallbackList', () => { it('Should call callbacks in reverse order', done => { const log: number[] = []; diff --git a/packages/node/test/integrations/spotlight.test.ts b/packages/node/test/integrations/spotlight.test.ts index a892677d0dd0..6b888c22edcd 100644 --- a/packages/node/test/integrations/spotlight.test.ts +++ b/packages/node/test/integrations/spotlight.test.ts @@ -2,9 +2,9 @@ import * as http from 'http'; import type { Envelope, EventEnvelope } from '@sentry/types'; import { createEnvelope, logger } from '@sentry/utils'; -import { NodeClient, spotlightIntegration } from '../../src'; -import { Spotlight } from '../../src/integrations'; -import { getDefaultNodeClientOptions } from '../helper/node-client-options'; +import { spotlightIntegration } from '../../src/integrations/spotlight'; +import { NodeClient } from '../../src/sdk/client'; +import { getDefaultNodeClientOptions } from '../helpers/getDefaultNodeClientOptions'; describe('Spotlight', () => { const loggerSpy = jest.spyOn(logger, 'warn'); @@ -17,12 +17,9 @@ describe('Spotlight', () => { const options = getDefaultNodeClientOptions(); const client = new NodeClient(options); - it('has a name and id', () => { - // eslint-disable-next-line deprecation/deprecation - const integration = new Spotlight(); + it('has a name', () => { + const integration = spotlightIntegration(); expect(integration.name).toEqual('Spotlight'); - // eslint-disable-next-line deprecation/deprecation - expect(Spotlight.id).toEqual('Spotlight'); }); it('registers a callback on the `beforeEnvelope` hook', () => { diff --git a/packages/node/test/integrations/undici.test.ts b/packages/node/test/integrations/undici.test.ts deleted file mode 100644 index 37a53b5e29c6..000000000000 --- a/packages/node/test/integrations/undici.test.ts +++ /dev/null @@ -1,526 +0,0 @@ -import * as http from 'http'; -import { - Transaction, - getActiveSpan, - getClient, - getCurrentScope, - getIsolationScope, - getMainCarrier, - getSpanDescendants, - setCurrentClient, - spanToJSON, - startSpan, - withIsolationScope, -} from '@sentry/core'; -import { spanToTraceHeader } from '@sentry/core'; -import { fetch } from 'undici'; - -import { NodeClient } from '../../src/client'; -import type { Undici, UndiciOptions } from '../../src/integrations/undici'; -import { nativeNodeFetchintegration } from '../../src/integrations/undici'; -import { getDefaultNodeClientOptions } from '../helper/node-client-options'; -import { conditionalTest } from '../utils'; - -const SENTRY_DSN = 'https://0@0.ingest.sentry.io/0'; - -beforeAll(async () => { - try { - await setupTestServer(); - } catch (e) { - const error = new Error('Undici integration tests are skipped because test server could not be set up.'); - // This needs lib es2022 and newer so marking as any - (error as any).cause = e; - throw e; - } -}); - -const DEFAULT_OPTIONS = getDefaultNodeClientOptions({ - dsn: SENTRY_DSN, - tracesSampler: () => true, - integrations: [nativeNodeFetchintegration()], - debug: true, -}); - -beforeEach(() => { - // Ensure we reset a potentially set acs to use the default - const sentry = getMainCarrier().__SENTRY__; - if (sentry) { - sentry.acs = undefined; - } - - getCurrentScope().clear(); - getIsolationScope().clear(); - const client = new NodeClient(DEFAULT_OPTIONS); - setCurrentClient(client); - client.init(); -}); - -afterEach(() => { - requestHeaders = {}; - setTestServerOptions({ statusCode: 200 }); -}); - -afterAll(() => { - getTestServer()?.close(); -}); - -conditionalTest({ min: 16 })('Undici integration', () => { - it.each([ - [ - 'simple url', - 'http://localhost:18100', - undefined, - { - description: 'GET http://localhost:18100/', - op: 'http.client', - data: expect.objectContaining({ - 'http.method': 'GET', - }), - }, - ], - [ - 'url with query', - 'http://localhost:18100?foo=bar', - undefined, - { - description: 'GET http://localhost:18100/', - op: 'http.client', - data: expect.objectContaining({ - 'http.method': 'GET', - 'http.query': '?foo=bar', - }), - }, - ], - [ - 'url with POST method', - 'http://localhost:18100', - { method: 'POST' }, - { - description: 'POST http://localhost:18100/', - data: expect.objectContaining({ - 'http.method': 'POST', - }), - }, - ], - [ - 'url with POST method', - 'http://localhost:18100', - { method: 'POST' }, - { - description: 'POST http://localhost:18100/', - data: expect.objectContaining({ - 'http.method': 'POST', - }), - }, - ], - [ - 'url with GET as default', - 'http://localhost:18100', - { method: undefined }, - { - description: 'GET http://localhost:18100/', - }, - ], - ])('creates a span with a %s', async (_: string, request, requestInit, expected) => { - await startSpan({ name: 'outer-span' }, async outerSpan => { - await fetch(request, requestInit); - - expect(outerSpan).toBeInstanceOf(Transaction); - const spans = getSpanDescendants(outerSpan); - - expect(spans.length).toBe(2); - - const span = spanToJSON(spans[1]); - expect(span).toEqual(expect.objectContaining(expected)); - }); - }); - - it('creates a span with internal errors', async () => { - await startSpan({ name: 'outer-span' }, async outerSpan => { - try { - await fetch('http://a-url-that-no-exists.com'); - } catch (e) { - // ignore - } - - expect(outerSpan).toBeInstanceOf(Transaction); - const spans = getSpanDescendants(outerSpan); - - expect(spans.length).toBe(2); - - const span = spans[1]; - expect(spanToJSON(span).status).toEqual('internal_error'); - }); - }); - - it('creates a span for invalid looking urls', async () => { - await startSpan({ name: 'outer-span' }, async outerSpan => { - try { - // Intentionally add // to the url - // fetch accepts this URL, but throws an error later on - await fetch('http://a-url-that-no-exists.com//'); - } catch (e) { - // ignore - } - - expect(outerSpan).toBeInstanceOf(Transaction); - const spans = getSpanDescendants(outerSpan); - - expect(spans.length).toBe(2); - - const spanJson = spanToJSON(spans[1]); - expect(spanJson.description).toEqual('GET http://a-url-that-no-exists.com//'); - expect(spanJson.status).toEqual('internal_error'); - }); - }); - - it('does not create a span for sentry requests', async () => { - await startSpan({ name: 'outer-span' }, async outerSpan => { - try { - await fetch(`${SENTRY_DSN}/sub/route`, { - method: 'POST', - }); - } catch (e) { - // ignore - } - - expect(outerSpan).toBeInstanceOf(Transaction); - const spans = getSpanDescendants(outerSpan); - - expect(spans.length).toBe(1); - }); - }); - - it('does not create a span if there is no active spans', async () => { - try { - await fetch(`${SENTRY_DSN}/sub/route`, { method: 'POST' }); - } catch (e) { - // ignore - } - - expect(getActiveSpan()).toBeUndefined(); - }); - - it('does create a span if `shouldCreateSpanForRequest` is defined', async () => { - await startSpan({ name: 'outer-span' }, async outerSpan => { - expect(outerSpan).toBeInstanceOf(Transaction); - expect(getSpanDescendants(outerSpan).length).toBe(1); - - const undoPatch = patchUndici({ shouldCreateSpanForRequest: url => url.includes('yes') }); - - await fetch('http://localhost:18100/no', { method: 'POST' }); - - expect(getSpanDescendants(outerSpan).length).toBe(1); - - await fetch('http://localhost:18100/yes', { method: 'POST' }); - - expect(getSpanDescendants(outerSpan).length).toBe(2); - - undoPatch(); - }); - }); - - // This flakes on CI for some reason: https://github.com/getsentry/sentry-javascript/pull/8449 - // eslint-disable-next-line jest/no-disabled-tests - it.skip('attaches the sentry trace and baggage headers if there is an active span', async () => { - expect.assertions(3); - - await withIsolationScope(async () => { - await startSpan({ name: 'outer-span' }, async outerSpan => { - expect(outerSpan).toBeInstanceOf(Transaction); - const spans = getSpanDescendants(outerSpan); - - await fetch('http://localhost:18100', { method: 'POST' }); - - expect(spans.length).toBe(2); - const span = spans[1]; - - expect(requestHeaders['sentry-trace']).toEqual(spanToTraceHeader(span)); - expect(requestHeaders['baggage']).toEqual( - `sentry-environment=production,sentry-public_key=0,sentry-trace_id=${ - span.spanContext().traceId - },sentry-sample_rate=1,sentry-transaction=test-transaction`, - ); - }); - }); - }); - - // This flakes on CI for some reason: https://github.com/getsentry/sentry-javascript/pull/8449 - // eslint-disable-next-line jest/no-disabled-tests - it.skip('attaches the sentry trace and baggage headers if there is no active span', async () => { - const scope = getCurrentScope(); - - await fetch('http://localhost:18100', { method: 'POST' }); - - const propagationContext = scope.getPropagationContext(); - - expect(requestHeaders['sentry-trace'].includes(propagationContext.traceId)).toBe(true); - expect(requestHeaders['baggage']).toEqual( - `sentry-environment=production,sentry-public_key=0,sentry-trace_id=${propagationContext.traceId},sentry-sample_rate=1,sentry-transaction=test-transaction,sentry-sampled=true`, - ); - }); - - // This flakes on CI for some reason: https://github.com/getsentry/sentry-javascript/pull/8449 - // eslint-disable-next-line jest/no-disabled-tests - it.skip('attaches headers if `shouldCreateSpanForRequest` does not create a span using propagation context', async () => { - const scope = getCurrentScope(); - const propagationContext = scope.getPropagationContext(); - - await startSpan({ name: 'outer-span' }, async outerSpan => { - expect(outerSpan).toBeInstanceOf(Transaction); - - const undoPatch = patchUndici({ shouldCreateSpanForRequest: url => url.includes('yes') }); - - await fetch('http://localhost:18100/no', { method: 'POST' }); - - expect(requestHeaders['sentry-trace']).toBeDefined(); - expect(requestHeaders['baggage']).toBeDefined(); - - expect(requestHeaders['sentry-trace'].includes(propagationContext.traceId)).toBe(true); - const firstSpanId = requestHeaders['sentry-trace'].split('-')[1]; - - await fetch('http://localhost:18100/yes', { method: 'POST' }); - - expect(requestHeaders['sentry-trace']).toBeDefined(); - expect(requestHeaders['baggage']).toBeDefined(); - - expect(requestHeaders['sentry-trace'].includes(propagationContext.traceId)).toBe(false); - - const secondSpanId = requestHeaders['sentry-trace'].split('-')[1]; - expect(firstSpanId).not.toBe(secondSpanId); - - undoPatch(); - }); - }); - - // This flakes on CI for some reason: https://github.com/getsentry/sentry-javascript/pull/8449 - // eslint-disable-next-line jest/no-disabled-tests - it.skip('uses tracePropagationTargets', async () => { - const client = new NodeClient({ ...DEFAULT_OPTIONS, tracePropagationTargets: ['/yes'] }); - setCurrentClient(client); - client.init(); - - await startSpan({ name: 'outer-span' }, async outerSpan => { - expect(outerSpan).toBeInstanceOf(Transaction); - const spans = getSpanDescendants(outerSpan); - - expect(spans.length).toBe(1); - - await fetch('http://localhost:18100/no', { method: 'POST' }); - - expect(spans.length).toBe(2); - - expect(requestHeaders['sentry-trace']).toBeUndefined(); - expect(requestHeaders['baggage']).toBeUndefined(); - - await fetch('http://localhost:18100/yes', { method: 'POST' }); - - expect(spans.length).toBe(3); - - expect(requestHeaders['sentry-trace']).toBeDefined(); - expect(requestHeaders['baggage']).toBeDefined(); - }); - }); - - it('adds a breadcrumb on request', async () => { - expect.assertions(1); - - const client = new NodeClient({ - ...DEFAULT_OPTIONS, - beforeBreadcrumb: breadcrumb => { - expect(breadcrumb).toEqual({ - category: 'http', - data: { - method: 'POST', - status_code: 200, - url: 'http://localhost:18100/', - }, - type: 'http', - timestamp: expect.any(Number), - }); - return breadcrumb; - }, - }); - setCurrentClient(client); - client.init(); - - await fetch('http://localhost:18100', { method: 'POST' }); - }); - - it('adds a breadcrumb on errored request', async () => { - expect.assertions(1); - - const client = new NodeClient({ - ...DEFAULT_OPTIONS, - beforeBreadcrumb: breadcrumb => { - expect(breadcrumb).toEqual({ - category: 'http', - data: { - method: 'GET', - url: 'http://a-url-that-no-exists.com/', - }, - level: 'error', - type: 'http', - timestamp: expect.any(Number), - }); - return breadcrumb; - }, - }); - setCurrentClient(client); - client.init(); - - try { - await fetch('http://a-url-that-no-exists.com'); - } catch (e) { - // ignore - } - }); - - it('does not add a breadcrumb if disabled', async () => { - expect.assertions(0); - - const undoPatch = patchUndici({ breadcrumbs: false }); - - await fetch('http://localhost:18100', { method: 'POST' }); - - undoPatch(); - }); - - describe('nativeNodeFetchIntegration', () => { - beforeEach(function () { - const options = getDefaultNodeClientOptions({ - dsn: 'https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.sentry.io/12312012', - tracesSampleRate: 1.0, - release: '1.0.0', - environment: 'production', - }); - const client = new NodeClient(options); - setCurrentClient(client); - }); - - it.each([ - [undefined, { a: true, b: true }], - [{}, { a: true, b: true }], - [{ tracing: true }, { a: true, b: true }], - [{ tracing: false }, { a: false, b: false }], - [ - { tracing: false, shouldCreateSpanForRequest: () => true }, - { a: false, b: false }, - ], - [ - { tracing: true, shouldCreateSpanForRequest: (url: string) => url === 'a' }, - { a: true, b: false }, - ], - ])('sets correct _shouldCreateSpan filter with options=%p', (options, expected) => { - // eslint-disable-next-line deprecation/deprecation - const actual = nativeNodeFetchintegration(options) as unknown as Undici; - - for (const [url, shouldBe] of Object.entries(expected)) { - expect(actual['_shouldCreateSpan'](url)).toEqual(shouldBe); - } - }); - - it('disables tracing spans if tracing is disabled in client', () => { - const client = new NodeClient( - getDefaultNodeClientOptions({ - dsn: SENTRY_DSN, - integrations: [nativeNodeFetchintegration()], - }), - ); - setCurrentClient(client); - - // eslint-disable-next-line deprecation/deprecation - const actual = nativeNodeFetchintegration() as unknown as Undici; - - expect(actual['_shouldCreateSpan']('a')).toEqual(false); - expect(actual['_shouldCreateSpan']('b')).toEqual(false); - }); - - it('enabled tracing spans if tracing is enabled in client', () => { - const client = new NodeClient( - getDefaultNodeClientOptions({ - dsn: SENTRY_DSN, - integrations: [nativeNodeFetchintegration()], - enableTracing: true, - }), - ); - setCurrentClient(client); - - // eslint-disable-next-line deprecation/deprecation - const actual = nativeNodeFetchintegration() as unknown as Undici; - - expect(actual['_shouldCreateSpan']('a')).toEqual(true); - expect(actual['_shouldCreateSpan']('b')).toEqual(true); - }); - }); -}); - -interface TestServerOptions { - statusCode: number; - responseHeaders?: Record; -} - -let testServer: http.Server | undefined; - -let requestHeaders: any = {}; - -let testServerOptions: TestServerOptions = { - statusCode: 200, -}; - -function setTestServerOptions(options: TestServerOptions): void { - testServerOptions = { ...options }; -} - -function getTestServer(): http.Server | undefined { - return testServer; -} - -function setupTestServer() { - testServer = http.createServer((req, res) => { - const chunks: Buffer[] = []; - - req.on('data', data => { - chunks.push(data); - }); - - req.on('end', () => { - requestHeaders = req.headers; - }); - - res.writeHead(testServerOptions.statusCode, testServerOptions.responseHeaders); - res.end(); - - // also terminate socket because keepalive hangs connection a bit - // eslint-disable-next-line deprecation/deprecation - res.connection?.end(); - }); - - testServer?.listen(18100); - - return new Promise(resolve => { - testServer?.on('listening', resolve); - }); -} - -function patchUndici(userOptions: Partial): () => void { - try { - const undici = getClient()!.getIntegrationByName!('Undici'); - // @ts-expect-error need to access private property - options = { ...undici._options }; - // @ts-expect-error need to access private property - undici._options = Object.assign(undici._options, userOptions); - } catch (_) { - throw new Error('Could not undo patching of undici'); - } - - return () => { - try { - const undici = getClient()!.getIntegrationByName!('Undici'); - // @ts-expect-error Need to override readonly property - undici!['_options'] = { ...options }; - } catch (_) { - throw new Error('Could not undo patching of undici'); - } - }; -} diff --git a/packages/node/test/manual/colorize.js b/packages/node/test/manual/colorize.js deleted file mode 100644 index 632d20aba4f7..000000000000 --- a/packages/node/test/manual/colorize.js +++ /dev/null @@ -1,16 +0,0 @@ -const COLOR_RESET = '\x1b[0m'; -const COLORS = { - green: '\x1b[32m', - red: '\x1b[31m', - yellow: '\x1b[33m', -}; - -function colorize(str, color) { - if (!(color in COLORS)) { - throw new Error(`Unknown color. Available colors: ${Object.keys(COLORS).join(', ')}`); - } - - return `${COLORS[color]}${str}${COLOR_RESET}`; -} - -module.exports = { colorize }; diff --git a/packages/node/test/manual/express-scope-separation/start.js b/packages/node/test/manual/express-scope-separation/start.js deleted file mode 100644 index c1981c0d0632..000000000000 --- a/packages/node/test/manual/express-scope-separation/start.js +++ /dev/null @@ -1,95 +0,0 @@ -const http = require('http'); -const express = require('express'); -const app = express(); -const Sentry = require('../../../build/cjs'); -const { colorize } = require('../colorize'); - -// don't log the test errors we're going to throw, so at a quick glance it doesn't look like the test itself has failed -global.console.error = () => null; - -function assertTags(actual, expected) { - if (JSON.stringify(actual) !== JSON.stringify(expected)) { - console.log(colorize('FAILED: Scope contains incorrect tags\n', 'red')); - console.log(colorize(`Got: ${JSON.stringify(actual)}\n`, 'red')); - console.log(colorize(`Expected: ${JSON.stringify(expected)}\n`, 'red')); - process.exit(1); - } -} - -let remaining = 3; - -function makeDummyTransport() { - return Sentry.createTransport({ recordDroppedEvent: () => undefined }, req => { - --remaining; - - if (!remaining) { - console.log(colorize('PASSED: All scopes contain correct tags\n', 'green')); - server.close(); - process.exit(0); - } - - return Promise.resolve({ - statusCode: 200, - }); - }); -} - -Sentry.init({ - dsn: 'http://test@example.com/1337', - transport: makeDummyTransport, - beforeSend(event) { - if (event.transaction === 'GET /foo') { - assertTags(event.tags, { - global: 'wat', - foo: 'wat', - }); - } else if (event.transaction === 'GET /bar') { - assertTags(event.tags, { - global: 'wat', - bar: 'wat', - }); - } else if (event.transaction === 'GET /baz') { - assertTags(event.tags, { - global: 'wat', - baz: 'wat', - }); - } else { - assertTags(event.tags, { - global: 'wat', - }); - } - - return event; - }, -}); - -Sentry.setTag('global', 'wat'); - -app.use(Sentry.Handlers.requestHandler()); - -app.get('/foo', req => { - Sentry.setTag('foo', 'wat'); - - throw new Error('foo'); -}); - -app.get('/bar', req => { - Sentry.setTag('bar', 'wat'); - - throw new Error('bar'); -}); - -app.get('/baz', req => { - Sentry.setTag('baz', 'wat'); - - throw new Error('baz'); -}); - -app.use(Sentry.Handlers.errorHandler()); - -const server = app.listen(0, () => { - const port = server.address().port; - http.get(`http://localhost:${port}/foo`); - http.get(`http://localhost:${port}/bar`); - http.get(`http://localhost:${port}/baz`); -}); diff --git a/packages/node/test/manual/memory-leak/.gitignore b/packages/node/test/manual/memory-leak/.gitignore deleted file mode 100644 index 8d5fa3beb48f..000000000000 --- a/packages/node/test/manual/memory-leak/.gitignore +++ /dev/null @@ -1 +0,0 @@ -large-module-dist.js diff --git a/packages/node/test/manual/memory-leak/README.md b/packages/node/test/manual/memory-leak/README.md deleted file mode 100644 index 844c6ffe7391..000000000000 --- a/packages/node/test/manual/memory-leak/README.md +++ /dev/null @@ -1,159 +0,0 @@ -# Manual Tests - -## How this works - -`express-patient.js` is an express app with a collection of endpoints that exercise various functionalities of -@sentry/node, including exception capturing, contexts, autobreadcrumbs, and the express middleware. - -It uses [memwatch-next](https://www.npmjs.com/package/memwatch-next) to record memory usage after each GC. `manager.js` -does some child process stuff to have a fresh patient process for each test scenario, while poke-patient.sh uses apache -bench to send a bunch of traffic so we can see what happens. - -## Routes and what we test - -The @sentry/node express middleware is used on all endpoints, so each request constitutes its own context. - -- `/hello`: just send a basic response without doing anything -- `/context/basic`: `setContext` call -- `/breadcrumbs/capture`: manual `captureBreadcrumb` call -- `/breadcrumbs/auto/console`: console log with console autoBreadcrumbs enabled -- `/breadcrumbs/auto/http`: send an http request with http autoBreadcrumbs enabled - - uses nock to mock the response, not actual request -- If the request has querystring param `doError=true`, we pass an error via Express's error handling mechanism with - `next(new Error(responseText))` which will then be captured by the @sentry/node express middleware error handler. - - We test all 5 above cases with and without `doError=true` - -We also have a `/gc` endpoint for forcing a garbage collection; this is used at the end of each test scenario to see -final memory usage. - -Note: there's a `/capture` endpoint which does a basic `captureException` call 1000 times. That's our current problem -child requiring some more investigation on its memory usage. - -## How to run it - -```bash -npm install memwatch-next nock -node manager.js -# in another tab send some traffic at it: -curl localhost:3000/capture -``` - -## Why this can't be more automated - -Some objects can have long lifecycles or not be cleaned up by GC when you think they would be, and so it isn't -straightforward to make the assertion "memory usage should have returned to baseline by now". Also, when the numbers -look bad, it's pretty obvious to a trained eye that they're bad, but it can be hard to quantify an exact threshold of -pass or fail. - -## Interpreting results - -Starting the manager and then running `ab -c 5 -n 5000 /context/basic && sleep 1 && curl localhost:3000/gc` will get us -this output: - -
    -``` -:[/Users/lewis/dev/raven-node/test/manual]#memleak-tests?$ node manager.js -starting child -patient is waiting to be poked on port 3000 -gc #1: min 0, max 0, est base 11639328, curr base 11639328 -gc #2: min 0, max 0, est base 11582672, curr base 11582672 -hit /context/basic for first time -gc #3: min 16864536, max 16864536, est base 16864536, curr base 16864536 -gc #4: min 14830680, max 16864536, est base 14830680, curr base 14830680 -gc #5: min 14830680, max 16864536, est base 16013904, curr base 16013904 -hit /gc for first time -gc #6: min 12115288, max 16864536, est base 12115288, curr base 12115288 -gc #7: min 11673824, max 16864536, est base 11673824, curr base 11673824 -``` -
    -This test stores some basic data in the request's Raven context, with the hope being for that context data to go out of scope and be garbage collected after the request is over. We can see that we start at a base of ~11.6MB, go up to ~16.8MB during the test, and then return to ~11.6MB. Everything checks out, no memory leak issue here. - -Back when we had a memory leak in `captureException`, if we started the manager and ran: - -```shell -ab -c 5 -n 5000 localhost:3000/context/basic?doError=true && sleep 5 && curl localhost:3000/gc -sleep 5 -curl localhost:3000/gc -sleep 10 -curl localhost:3000/gc -sleep 15 -curl localhost:3000/gc -``` - -we'd get this output: - -
    -``` -[/Users/lewis/dev/raven-node/test/manual]#memleak-tests?$ node manager.js -starting child -patient is waiting to be poked on port 3000 -gc #1: min 0, max 0, est base 11657056, curr base 11657056 -gc #2: min 0, max 0, est base 11599392, curr base 11599392 -hit /context/basic?doError=true for first time -gc #3: min 20607752, max 20607752, est base 20607752, curr base 20607752 -gc #4: min 20607752, max 20969872, est base 20969872, curr base 20969872 -gc #5: min 19217632, max 20969872, est base 19217632, curr base 19217632 -gc #6: min 19217632, max 21025056, est base 21025056, curr base 21025056 -gc #7: min 19217632, max 21096656, est base 21096656, curr base 21096656 -gc #8: min 19085432, max 21096656, est base 19085432, curr base 19085432 -gc #9: min 19085432, max 22666768, est base 22666768, curr base 22666768 -gc #10: min 19085432, max 22666768, est base 22487320, curr base 20872288 -gc #11: min 19085432, max 22708656, est base 22509453, curr base 22708656 -gc #12: min 19085432, max 22708656, est base 22470302, curr base 22117952 -gc #13: min 19085432, max 22708656, est base 22440838, curr base 22175664 -gc #14: min 19085432, max 22829952, est base 22479749, curr base 22829952 -gc #15: min 19085432, max 25273504, est base 22759124, curr base 25273504 -gc #16: min 19085432, max 25273504, est base 22707814, curr base 22246024 -gc #17: min 19085432, max 33286216, est base 23765654, curr base 33286216 -gc #18: min 19085432, max 33286216, est base 23863713, curr base 24746248 -gc #19: min 19085432, max 33286216, est base 23685980, curr base 22086392 -gc #20: min 19085432, max 33286216, est base 23705022, curr base 23876400 -gc #21: min 19085432, max 33286216, est base 23769947, curr base 24354272 -gc #22: min 19085432, max 33286216, est base 23987724, curr base 25947720 -gc #23: min 19085432, max 33286216, est base 24636946, curr base 30479952 -gc #24: min 19085432, max 33286216, est base 24668561, curr base 24953096 -gc #25: min 19085432, max 33286216, est base 24750980, curr base 25492760 -gc #26: min 19085432, max 33286216, est base 24956242, curr base 26803600 -gc #27: min 19085432, max 33286216, est base 25127122, curr base 26665048 -gc #28: min 19085432, max 33286216, est base 25357309, curr base 27428992 -gc #29: min 19085432, max 33286216, est base 25519102, curr base 26975240 -gc #30: min 19085432, max 33286216, est base 25830428, curr base 28632368 -gc #31: min 19085432, max 33286216, est base 26113116, curr base 28657312 -gc #32: min 19085432, max 33286216, est base 26474999, curr base 29731952 -gc #33: min 19085432, max 41429616, est base 27970460, curr base 41429616 -gc #34: min 19085432, max 41429616, est base 29262386, curr base 40889728 -gc #35: min 19085432, max 41429616, est base 29402336, curr base 30661888 -gc #36: min 19085432, max 41429616, est base 29602979, curr base 31408768 -gc #37: min 19085432, max 42724544, est base 30915135, curr base 42724544 -gc #38: min 19085432, max 42724544, est base 31095390, curr base 32717688 -gc #39: min 19085432, max 42724544, est base 31907458, curr base 39216072 -gc #40: min 19085432, max 42724544, est base 32093021, curr base 33763088 -gc #41: min 19085432, max 42724544, est base 32281586, curr base 33978672 -gc #42: min 19085432, max 42724544, est base 32543090, curr base 34896632 -gc #43: min 19085432, max 42724544, est base 32743548, curr base 34547672 -gc #44: min 19085432, max 42724544, est base 33191109, curr base 37219160 -gc #45: min 19085432, max 42724544, est base 33659862, curr base 37878640 -gc #46: min 19085432, max 42724544, est base 34162262, curr base 38683864 -gc #47: min 19085432, max 42724544, est base 34624103, curr base 38780680 -gc #48: min 19085432, max 42724544, est base 35125267, curr base 39635752 -gc #49: min 19085432, max 42724544, est base 35547207, curr base 39344672 -gc #50: min 19085432, max 42724544, est base 35827942, curr base 38354560 -gc #51: min 19085432, max 42724544, est base 36185625, curr base 39404776 -gc #52: min 19085432, max 52995432, est base 37866605, curr base 52995432 -gc #53: min 19085432, max 52995432, est base 39230884, curr base 51509400 -gc #54: min 19085432, max 52995432, est base 39651220, curr base 43434248 -gc #55: min 19085432, max 52995432, est base 40010377, curr base 43242792 -gc #56: min 19085432, max 52995432, est base 40443827, curr base 44344880 -gc #57: min 19085432, max 52995432, est base 40979365, curr base 45799208 -gc #58: min 19085432, max 52995432, est base 41337723, curr base 44562952 -gc #59: min 19085432, max 57831608, est base 42987111, curr base 57831608 -hit /gc for first time -gc #60: min 19085432, max 57831608, est base 42763791, curr base 40753920 -gc #61: min 19085432, max 57831608, est base 42427528, curr base 39401168 -gc #62: min 19085432, max 57831608, est base 42125779, curr base 39410040 -gc #63: min 19085432, max 57831608, est base 41850385, curr base 39371848 -gc #64: min 19085432, max 57831608, est base 41606578, curr base 39412320 -gc #65: min 19085432, max 57831608, est base 41386124, curr base 39402040 -``` -
    -This test, after storing some basic data in the request's SDK context, generates an error which SDK's express error handling middleware will capture. We can see that we started at a base of ~11.6MB, climbed steadily throughout the test to ~40-50MB toward the end, returned to ~39.4MB after the test ends, and were then still at ~39.4MB after 30 seconds and more GCing. This was worrysome, being 30MB over our baseline after 1000 captures. Something was up with capturing exceptions and we uncovered and fixed a memory leak as a result. Now the test returns to a baseline of ~13MB; the slight increase over 11.6MB is due to some warmup costs, but the marginal cost of additional capturing is zero (i.e. we return to that ~13MB baseline whether we do 1000 captures or 5000). diff --git a/packages/node/test/manual/memory-leak/context-memory.js b/packages/node/test/manual/memory-leak/context-memory.js deleted file mode 100644 index 6c124d542ce5..000000000000 --- a/packages/node/test/manual/memory-leak/context-memory.js +++ /dev/null @@ -1,25 +0,0 @@ -const Sentry = require('../../../build/cjs'); - -Sentry.init({ dsn: 'https://public@app.getsentry.com/12345' }); - -// We create a bunch of contexts, capture some breadcrumb data in all of them, -// then watch memory usage. It'll go up to ~40 megs then after 10 or 20 seconds -// gc will drop it back to ~5. - -console.log(process.memoryUsage()); - -for (let i = 0; i < 10000; i++) { - Sentry.withScope(() => { - Sentry.addBreadcrumb({ message: Array(1000).join('.') }); - - setTimeout(() => { - Sentry.addBreadcrumb({ message: Array(1000).join('a') }); - }, 2000); - }); -} - -console.log(process.memoryUsage()); - -setInterval(function () { - console.log(process.memoryUsage()); -}, 1000); diff --git a/packages/node/test/manual/memory-leak/express-patient.js b/packages/node/test/manual/memory-leak/express-patient.js deleted file mode 100644 index ad9ed267bde4..000000000000 --- a/packages/node/test/manual/memory-leak/express-patient.js +++ /dev/null @@ -1,146 +0,0 @@ -const Sentry = require('../../../build/cjs'); - -Sentry.init({ dsn: 'https://public@app.getsentry.com/12345' }); - -const util = require('util'); -const http = require('http'); -const nock = require('nock'); - -// have to call this for each request :/ ref https://github.com/node-nock/nock#read-this---about-interceptors -function nockRequest() { - nock('https://app.getsentry.com').filteringRequestBody(/.*/, '*').post(/.*/, '*').reply(200, 'OK'); -} - -const memwatch = require('memwatch-next'); -memwatch.on('stats', function (stats) { - process._rawDebug( - util.format( - 'gc #%d: min %d, max %d, est base %d, curr base %d', - stats.num_full_gc, - stats.min, - stats.max, - stats.estimated_base, - stats.current_base, - ), - ); -}); - -const express = require('express'); -const app = express(); - -const hitBefore = {}; - -app.use(Sentry.Handlers.requestHandler()); - -app.use((req, res, next) => { - if (!hitBefore[req.url]) { - hitBefore[req.url] = true; - process._rawDebug('hit ' + req.url + ' for first time'); - } - next(); -}); - -app.get('/context/basic', (req, res, next) => { - Sentry.setExtra('example', 'hey look we set some example context data yay'); - - res.textToSend = 'hello there! we set some stuff to the context'; - next(); -}); - -app.get('/breadcrumbs/capture', (req, res, next) => { - Sentry.captureBreadcrumb({ - message: 'Captured example breadcrumb', - category: 'log', - data: { - example: 'hey look we captured this example breadcrumb yay', - }, - }); - res.textToSend = 'hello there! we captured an example breadcrumb'; - next(); -}); - -app.get('/breadcrumbs/auto/console', (req, res, next) => { - console.log('hello there! i am printing to the console!'); - res.textToSend = 'hello there! we printed to the console'; - next(); -}); - -app.get('/breadcrumbs/auto/http', (req, res, next) => { - const scope = nock('http://www.example.com').get('/hello').reply(200, 'hello world'); - - http - .get('http://www.example.com/hello', function (nockRes) { - scope.done(); - res.textToSend = 'hello there! we got hello world from example.com'; - next(); - }) - .on('error', next); -}); - -app.get('/hello', (req, res, next) => { - res.textToSend = 'hello!'; - next(); -}); - -app.get('/gc', (req, res, next) => { - memwatch.gc(); - res.textToSend = 'collected garbage'; - next(); -}); - -app.get('/shutdown', (req, res, next) => { - setTimeout(function () { - server.close(function () { - process.exit(); - }); - }, 100); - return res.send('shutting down'); -}); - -app.get('/capture', (req, res, next) => { - for (let i = 0; i < 1000; ++i) { - nockRequest(); - Sentry.captureException(new Error('oh no an exception to capture')); - } - memwatch.gc(); - res.textToSend = 'capturing an exception!'; - next(); -}); - -app.get('/capture_large_source', (req, res, next) => { - nockRequest(); - - // largeModule.run recurses 1000 times, largeModule is a 5MB file - // if we read the largeModule source once for each frame, we'll use a ton of memory - const largeModule = require('./large-module-dist'); - - try { - largeModule.run(); - } catch (e) { - Sentry.captureException(e); - } - - memwatch.gc(); - res.textToSend = 'capturing an exception!'; - next(); -}); - -app.use((req, res, next) => { - if (req.query.doError) { - nockRequest(); - return next(new Error(res.textToSend)); - } - return res.send(res.textToSend); -}); - -app.use(Sentry.Handlers.errorHandler()); - -app.use((err, req, res, next) => { - return res.status(500).send('oh no there was an error: ' + err.message); -}); - -const server = app.listen(0, () => { - const port = server.address().port; - process._rawDebug(`patient is waiting to be poked on port ${port}`); - memwatch.gc(); -}); diff --git a/packages/node/test/manual/memory-leak/large-module-src.js b/packages/node/test/manual/memory-leak/large-module-src.js deleted file mode 100644 index cc9a07e4d4c0..000000000000 --- a/packages/node/test/manual/memory-leak/large-module-src.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -function run(n) { - if (n == null) return run(1000); - if (n === 0) throw new Error('we did it!'); - console.log('run ' + n); - return run(n - 1); -} - -module.exports.run = run; - -// below is 5MB worth of 'A', so reading this file multiple times concurrently will use lots of memory -var a = '{{template}}'; diff --git a/packages/node/test/manual/memory-leak/manager.js b/packages/node/test/manual/memory-leak/manager.js deleted file mode 100644 index 616c82f8fe26..000000000000 --- a/packages/node/test/manual/memory-leak/manager.js +++ /dev/null @@ -1,33 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const child_process = require('child_process'); -const serverPath = path.join(__dirname, 'express-patient.js'); -let child; - -function generateLargeModule() { - fs.writeFileSync( - path.join(__dirname, 'large-module-dist.js'), - fs - .readFileSync(path.join(__dirname, 'large-module-src.js')) - .toString() - .replace('{{template}}', 'A'.repeat(5 * 1024 * 1024)), - ); -} - -if (!fs.existsSync(path.join(__dirname, 'large-module-dist.js'))) { - console.log('Missing large-module-dist.js file... generating...'); - generateLargeModule(); -} - -function startChild() { - console.log('starting child'); - child = child_process.spawn('node', [serverPath]); - child.stdout.pipe(process.stdout); - child.stderr.pipe(process.stderr); - child.on('exit', function () { - console.log('child exited'); - startChild(); - }); -} - -startChild(); diff --git a/packages/node/test/manual/memory-leak/poke-patient.sh b/packages/node/test/manual/memory-leak/poke-patient.sh deleted file mode 100755 index 8e87ca355928..000000000000 --- a/packages/node/test/manual/memory-leak/poke-patient.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/sh - -gc() { - sleep 2 - curl localhost:3000/gc -} - -gc_restart() { - gc - sleep 2 - curl localhost:3000/shutdown - sleep 2 -} - -curl localhost:3000/capture -gc -gc -curl localhost:3000/capture -gc -gc_restart - -curl localhost:3000/capture_large_source -gc -gc -gc_restart - -ab -c 5 -n 5000 localhost:3000/hello -gc_restart - -ab -c 5 -n 5000 localhost:3000/context/basic -gc_restart - -ab -c 5 -n 5000 localhost:3000/breadcrumbs/capture -gc_restart - -ab -c 5 -n 5000 localhost:3000/breadcrumbs/auto/console -gc_restart - -ab -c 5 -n 5000 localhost:3000/breadcrumbs/auto/http -gc_restart - - -ab -c 5 -n 2000 localhost:3000/hello?doError=true -gc_restart - -ab -c 5 -n 2000 localhost:3000/context/basic?doError=true -gc_restart - -ab -c 5 -n 2000 localhost:3000/breadcrumbs/capture?doError=true -gc_restart - -ab -c 5 -n 2000 localhost:3000/breadcrumbs/auto/console?doError=true -gc_restart - -ab -c 5 -n 2000 localhost:3000/breadcrumbs/auto/http?doError=true -gc_restart diff --git a/packages/node/test/manual/release-health/runner.js b/packages/node/test/manual/release-health/runner.js deleted file mode 100644 index 713ae5d10c3f..000000000000 --- a/packages/node/test/manual/release-health/runner.js +++ /dev/null @@ -1,53 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const { spawn } = require('child_process'); -const { colorize } = require('../colorize'); - -const scenariosDirs = ['session-aggregates', 'single-session']; -const scenarios = []; - -for (const dir of scenariosDirs) { - const scenarioDir = path.resolve(__dirname, dir); - const filenames = fs.readdirSync(scenarioDir); - const paths = filenames.map(filename => [filename, path.resolve(scenarioDir, filename)]); - scenarios.push(...paths); -} - -const processes = scenarios.map(([filename, filepath]) => { - return new Promise(resolve => { - const scenarioProcess = spawn('node', [filepath], { timeout: 10000 }); - const output = []; - const errors = []; - - scenarioProcess.stdout.on('data', data => { - output.push(data.toString()); - }); - - scenarioProcess.stderr.on('data', data => { - errors.push(data.toString()); - }); - - scenarioProcess.on('exit', code => { - if (code === 0) { - console.log(colorize(`PASSED: ${filename}`, 'green')); - } else { - console.log(colorize(`FAILED: ${filename}`, 'red')); - - if (output.length) { - console.log(colorize(output.join('\n'), 'yellow')); - } - if (errors.length) { - console.log(colorize(errors.join('\n'), 'yellow')); - } - } - - resolve(code); - }); - }); -}); - -Promise.all(processes).then(codes => { - if (codes.some(code => code !== 0)) { - process.exit(1); - } -}); diff --git a/packages/node/test/manual/release-health/session-aggregates/aggregates-disable-single-session.js b/packages/node/test/manual/release-health/session-aggregates/aggregates-disable-single-session.js deleted file mode 100644 index c9b5f934bcff..000000000000 --- a/packages/node/test/manual/release-health/session-aggregates/aggregates-disable-single-session.js +++ /dev/null @@ -1,98 +0,0 @@ -const http = require('http'); -const express = require('express'); -const app = express(); -const Sentry = require('../../../../build/cjs'); -const { assertSessions } = require('../test-utils'); - -function cleanUpAndExitSuccessfully() { - server.close(); - clearInterval(flusher._intervalId); - process.exit(0); -} - -function assertSessionAggregates(session, expected) { - if (!session.aggregates) { - return; - } - // For loop is added here just in the rare occasion that the session count do not land in the same aggregate - // bucket - session.aggregates.forEach(function (_, idx) { - delete session.aggregates[idx].started; - // Session Aggregates keys need to be ordered for JSON.stringify comparison - const ordered = Object.keys(session.aggregates[idx]) - .sort() - .reduce((obj, key) => { - obj[key] = session.aggregates[idx][key]; - return obj; - }, {}); - session.aggregates[idx] = ordered; - }); - assertSessions(session, expected); -} - -function makeDummyTransport() { - return Sentry.createTransport({ recordDroppedEvent: () => undefined }, req => { - const sessionEnv = req.body - .split('\n') - .filter(l => !!l) - .map(e => JSON.parse(e)); - - assertSessionAggregates(sessionEnv[2], { - attrs: { release: '1.1' }, - aggregates: [{ crashed: 2, errored: 1, exited: 1 }], - }); - - cleanUpAndExitSuccessfully(); - - return Promise.resolve({ - statusCode: 200, - }); - }); -} - -Sentry.init({ - dsn: 'http://test@example.com/1337', - release: '1.1', - transport: makeDummyTransport, - autoSessionTracking: true, -}); -/** - * Test that ensures that when `autoSessionTracking` is enabled and the express `requestHandler` middleware is used - * then Single Session should be disabled in favor of sending SessionAggregates. - */ - -app.use(Sentry.Handlers.requestHandler()); - -// Hack that resets the 60s default flush interval, and replaces it with just a one second interval -const flusher = Sentry.getClient()._sessionFlusher; -clearInterval(flusher._intervalId); -flusher._intervalId = setInterval(() => flusher.flush(), 1000); - -app.get('/foo', (req, res, next) => { - res.send('Success'); - next(); -}); - -app.get('/bar', (req, res, next) => { - throw new Error('bar'); -}); - -app.get('/baz', (req, res, next) => { - try { - throw new Error('hey there'); - } catch (e) { - Sentry.captureException(e); - } - res.send('Caught Exception: Baz'); - next(); -}); - -app.use(Sentry.Handlers.errorHandler()); - -const server = app.listen(0, () => { - const port = server.address().port; - http.get(`http://localhost:${port}/foo`); - http.get(`http://localhost:${port}/bar`); - http.get(`http://localhost:${port}/bar`); - http.get(`http://localhost:${port}/baz`); -}); diff --git a/packages/node/test/manual/release-health/single-session/caught-exception-errored-session.js b/packages/node/test/manual/release-health/single-session/caught-exception-errored-session.js deleted file mode 100644 index b2c2b0c9d265..000000000000 --- a/packages/node/test/manual/release-health/single-session/caught-exception-errored-session.js +++ /dev/null @@ -1,65 +0,0 @@ -const Sentry = require('../../../../build/cjs'); -const { assertSessions, constructStrippedSessionObject, validateSessionCountFunction } = require('../test-utils'); - -const sessionCounts = { - sessionCounter: 0, - expectedSessions: 2, -}; - -validateSessionCountFunction(sessionCounts); - -function makeDummyTransport() { - return Sentry.createTransport({ recordDroppedEvent: () => undefined }, req => { - const payload = req.body - .split('\n') - .filter(l => !!l) - .map(e => JSON.parse(e)); - const isSessionPayload = payload[1].type === 'session'; - - if (isSessionPayload) { - sessionCounts.sessionCounter++; - - if (sessionCounts.sessionCounter === 1) { - assertSessions(constructStrippedSessionObject(payload[2]), { - init: true, - status: 'ok', - errors: 1, - release: '1.1', - }); - } - - if (sessionCounts.sessionCounter === 2) { - assertSessions(constructStrippedSessionObject(payload[2]), { - init: false, - status: 'exited', - errors: 1, - release: '1.1', - }); - } - } - - return Promise.resolve({ - statusCode: 200, - }); - }); -} - -Sentry.init({ - dsn: 'http://test@example.com/1337', - release: '1.1', - transport: makeDummyTransport, - autoSessionTracking: true, -}); - -/** - * The following code snippet will capture exceptions of `mechanism.handled` equal to `true`, and so these sessions - * are treated as Errored Sessions. - * In this case, we have two session updates sent; First Session sent is due to the call to CaptureException that - * extracts event data and uses it to update the Session and sends it. The second session update is sent on the - * `beforeExit` event which happens right before the process exits. - */ -try { - throw new Error('hey there'); -} catch (e) { - Sentry.captureException(e); -} diff --git a/packages/node/test/manual/release-health/single-session/errors-in-session-capped-to-one.js b/packages/node/test/manual/release-health/single-session/errors-in-session-capped-to-one.js deleted file mode 100644 index b2eef0d2df72..000000000000 --- a/packages/node/test/manual/release-health/single-session/errors-in-session-capped-to-one.js +++ /dev/null @@ -1,59 +0,0 @@ -const Sentry = require('../../../../build/cjs'); -const { assertSessions, constructStrippedSessionObject, validateSessionCountFunction } = require('../test-utils'); - -const sessionCounts = { - sessionCounter: 0, - expectedSessions: 2, -}; - -validateSessionCountFunction(sessionCounts); - -function makeDummyTransport() { - return Sentry.createTransport({ recordDroppedEvent: () => undefined }, req => { - const payload = req.body - .split('\n') - .filter(l => !!l) - .map(e => JSON.parse(e)); - const isSessionPayload = payload[1].type === 'session'; - - if (isSessionPayload) { - sessionCounts.sessionCounter++; - - if (sessionCounts.sessionCounter === 1) { - assertSessions(constructStrippedSessionObject(payload[2]), { - init: true, - status: 'ok', - errors: 1, - release: '1.1', - }); - } - - if (sessionCounts.sessionCounter === 2) { - assertSessions(constructStrippedSessionObject(payload[2]), { - init: false, - status: 'exited', - errors: 1, - release: '1.1', - }); - } - } - - return Promise.resolve({ - statusCode: 200, - }); - }); -} - -Sentry.init({ - dsn: 'http://test@example.com/1337', - release: '1.1', - transport: makeDummyTransport, - autoSessionTracking: true, -}); -/** - * The following code snippet will throw multiple errors, and thereby send session updates everytime an error is - * captured. However, the number of errors in the session should be capped at 1, regardless of how many errors there are - */ -for (let i = 0; i < 2; i++) { - Sentry.captureException(new Error('hello world')); -} diff --git a/packages/node/test/manual/release-health/single-session/healthy-session.js b/packages/node/test/manual/release-health/single-session/healthy-session.js deleted file mode 100644 index 49420b0f23b5..000000000000 --- a/packages/node/test/manual/release-health/single-session/healthy-session.js +++ /dev/null @@ -1,43 +0,0 @@ -const Sentry = require('../../../../build/cjs'); -const { assertSessions, constructStrippedSessionObject, validateSessionCountFunction } = require('../test-utils'); - -const sessionCounts = { - sessionCounter: 0, - expectedSessions: 1, -}; - -validateSessionCountFunction(sessionCounts); - -function makeDummyTransport() { - return Sentry.createTransport({ recordDroppedEvent: () => undefined }, req => { - sessionCounts.sessionCounter++; - - const sessionEnv = req.body - .split('\n') - .filter(l => !!l) - .map(e => JSON.parse(e)); - - assertSessions(constructStrippedSessionObject(sessionEnv[2]), { - init: true, - status: 'exited', - errors: 0, - release: '1.1', - }); - - return Promise.resolve({ - statusCode: 200, - }); - }); -} - -Sentry.init({ - dsn: 'http://test@example.com/1337', - release: '1.1', - transport: makeDummyTransport, - autoSessionTracking: true, -}); - -/** - * This script or process, start a Session on init object, and calls endSession on `beforeExit` of the process, which - * sends a healthy session to the Server. - */ diff --git a/packages/node/test/manual/release-health/single-session/terminal-state-sessions-sent-once.js b/packages/node/test/manual/release-health/single-session/terminal-state-sessions-sent-once.js deleted file mode 100644 index 40f95736d4d4..000000000000 --- a/packages/node/test/manual/release-health/single-session/terminal-state-sessions-sent-once.js +++ /dev/null @@ -1,59 +0,0 @@ -const Sentry = require('../../../../build/cjs'); -const { assertSessions, constructStrippedSessionObject, validateSessionCountFunction } = require('../test-utils'); - -const sessionCounts = { - sessionCounter: 0, - expectedSessions: 1, -}; - -validateSessionCountFunction(sessionCounts); - -function makeDummyTransport() { - return Sentry.createTransport({ recordDroppedEvent: () => undefined }, req => { - const payload = req.body - .split('\n') - .filter(l => !!l) - .map(e => JSON.parse(e)); - const isSessionPayload = payload[1].type === 'session'; - - if (isSessionPayload) { - sessionCounts.sessionCounter++; - - assertSessions(constructStrippedSessionObject(payload[2]), { - init: true, - status: 'crashed', - errors: 1, - release: '1.1', - }); - } - - return Promise.resolve({ - statusCode: 200, - }); - }); -} - -Sentry.init({ - dsn: 'http://test@example.com/1337', - release: '1.1', - transport: makeDummyTransport, - autoSessionTracking: true, -}); - -/** - * The following code snippet will throw an exception of `mechanism.handled` equal to `false`, and so this session - * is treated as a Crashed Session. - * However we want to ensure that once a crashed terminal state is achieved, no more session updates are sent regardless - * of whether more crashes happen or not - */ -new Promise(function (resolve, reject) { - reject(); -}).then(function () { - console.log('Promise Resolved'); -}); - -new Promise(function (resolve, reject) { - reject(); -}).then(function () { - console.log('Promise Resolved'); -}); diff --git a/packages/node/test/manual/release-health/single-session/uncaught-exception-crashed-session.js b/packages/node/test/manual/release-health/single-session/uncaught-exception-crashed-session.js deleted file mode 100644 index 1c111e0d4f31..000000000000 --- a/packages/node/test/manual/release-health/single-session/uncaught-exception-crashed-session.js +++ /dev/null @@ -1,40 +0,0 @@ -const Sentry = require('../../../../build/cjs'); -const { assertSessions, constructStrippedSessionObject } = require('../test-utils'); - -function makeDummyTransport() { - return Sentry.createTransport({ recordDroppedEvent: () => undefined }, req => { - if (req.category === 'session') { - sessionCounts.sessionCounter++; - const sessionEnv = req.body - .split('\n') - .filter(l => !!l) - .map(e => JSON.parse(e)); - - assertSessions(constructStrippedSessionObject(sessionEnv[2]), { - init: true, - status: 'crashed', - errors: 1, - release: '1.1', - }); - } - - // We need to explicitly exit process early here to allow for 0 exit code - process.exit(0); - }); -} - -Sentry.init({ - dsn: 'http://test@example.com/1337', - release: '1.1', - transport: makeDummyTransport, - autoSessionTracking: true, -}); - -/** - * The following code snippet will throw an exception of `mechanism.handled` equal to `false`, and so this session - * is considered a Crashed Session. - * In this case, we have only session update that is sent, which is sent due to the call to CaptureException that - * extracts event data and uses it to update the Session and send it. No secondary session update in this case because - * we explicitly exit the process in the onUncaughtException handler and so the `beforeExit` event is not fired. - */ -throw new Error('test error'); diff --git a/packages/node/test/manual/release-health/single-session/unhandled-rejection-crashed-session.js b/packages/node/test/manual/release-health/single-session/unhandled-rejection-crashed-session.js deleted file mode 100644 index d2c5e6cf11d3..000000000000 --- a/packages/node/test/manual/release-health/single-session/unhandled-rejection-crashed-session.js +++ /dev/null @@ -1,54 +0,0 @@ -const Sentry = require('../../../../build/cjs'); -const { assertSessions, constructStrippedSessionObject, validateSessionCountFunction } = require('../test-utils'); - -const sessionCounts = { - sessionCounter: 0, - expectedSessions: 1, -}; - -validateSessionCountFunction(sessionCounts); - -function makeDummyTransport() { - return Sentry.createTransport({ recordDroppedEvent: () => undefined }, req => { - const payload = req.body - .split('\n') - .filter(l => !!l) - .map(e => JSON.parse(e)); - const isSessionPayload = payload[1].type === 'session'; - - if (isSessionPayload) { - sessionCounts.sessionCounter++; - - assertSessions(constructStrippedSessionObject(payload[2]), { - init: true, - status: 'crashed', - errors: 1, - release: '1.1', - }); - } - - return Promise.resolve({ - statusCode: 200, - }); - }); -} - -Sentry.init({ - dsn: 'http://test@example.com/1337', - release: '1.1', - transport: makeDummyTransport, - autoSessionTracking: true, -}); - -/** - * The following code snippet will throw an exception of `mechanism.handled` equal to `false`, and so this session - * is treated as a Crashed Session. - * In this case, we have two session updates sent; First Session sent is due to the call to CaptureException that - * extracts event data and uses it to update the Session and sends it. The second session update is sent on the - * `beforeExit` event which happens right before the process exits. - */ -new Promise(function (resolve, reject) { - reject(); -}).then(function () { - console.log('Promise Resolved'); -}); diff --git a/packages/node/test/manual/release-health/test-utils.js b/packages/node/test/manual/release-health/test-utils.js deleted file mode 100644 index 2d22a760c76f..000000000000 --- a/packages/node/test/manual/release-health/test-utils.js +++ /dev/null @@ -1,31 +0,0 @@ -function assertSessions(actual, expected) { - actual = JSON.stringify(actual); - expected = JSON.stringify(expected); - if (actual !== expected) { - process.stdout.write(`Expected Session:\n ${expected}\nActual Session:\n ${actual}`); - process.exit(1); - } -} - -function constructStrippedSessionObject(actual) { - const { - init, - status, - errors, - attrs: { release }, - did, - } = actual; - return { init, status, errors, release, did }; -} - -function validateSessionCountFunction(sessionCounts) { - process.on('exit', () => { - const { sessionCounter, expectedSessions } = sessionCounts; - if (sessionCounter !== expectedSessions) { - process.stdout.write(`Expected Session Count: ${expectedSessions}\nActual Session Count: ${sessionCounter}`); - process.exitCode = 1; - } - }); -} - -module.exports = { assertSessions, constructStrippedSessionObject, validateSessionCountFunction }; diff --git a/packages/node/test/manual/webpack-async-context/index.js b/packages/node/test/manual/webpack-async-context/index.js deleted file mode 100644 index 445b277d79aa..000000000000 --- a/packages/node/test/manual/webpack-async-context/index.js +++ /dev/null @@ -1,52 +0,0 @@ -const Sentry = require('../../../build/cjs'); -const { colorize } = require('../colorize'); - -let remaining = 2; - -function makeDummyTransport() { - return Sentry.createTransport({ recordDroppedEvent: () => undefined }, req => { - --remaining; - - if (!remaining) { - console.log(colorize('PASSED: Webpack Node Domain test OK!\n', 'green')); - process.exit(0); - } - - return Promise.resolve({ - status: 'success', - }); - }); -} - -Sentry.init({ - dsn: 'https://a@example.com/1', - transport: makeDummyTransport, - beforeSend(event) { - if (event.message === 'inside') { - if (event.tags.a !== 'x' && event.tags.b !== 'c') { - console.log(colorize('FAILED: Scope contains incorrect tags\n', 'red')); - console.log(colorize(`Got: ${JSON.stringify(event.tags)}\n`, 'red')); - console.log(colorize(`Expected: Object including { a: 'x', b: 'c' }\n`, 'red')); - process.exit(1); - } - } - if (event.message === 'outside') { - if (event.tags.a !== 'b') { - console.log(colorize('FAILED: Scope contains incorrect tags\n', 'red')); - console.log(colorize(`Got: ${JSON.stringify(event.tags)}\n`, 'red')); - console.log(colorize(`Expected: Object including { a: 'b' }\n`, 'red')); - process.exit(1); - } - } - return event; - }, -}); - -Sentry.setTag('a', 'b'); - -Sentry.withIsolationScope(() => { - Sentry.setTag('a', 'x'); - Sentry.captureMessage('inside'); -}); - -Sentry.captureMessage('outside'); diff --git a/packages/node/test/manual/webpack-async-context/npm-build.js b/packages/node/test/manual/webpack-async-context/npm-build.js deleted file mode 100644 index eac357b10f36..000000000000 --- a/packages/node/test/manual/webpack-async-context/npm-build.js +++ /dev/null @@ -1,51 +0,0 @@ -const path = require('path'); -const webpack = require('webpack'); -const { execSync } = require('child_process'); - -// Webpack test does not work in Node 18 and above. -if (Number(process.versions.node.split('.')[0]) >= 18) { - process.exit(0); -} - -// biome-ignore format: Follow-up for prettier -webpack( - { - entry: './index.js', - output: { - path: path.resolve(__dirname, 'dist'), - filename: 'bundle.js', - }, - target: 'node', - mode: 'development', - }, - function (err, stats) { - if (err) { - console.error(err.stack || err); - if (err.details) { - console.error(err.details); - } - return; - } - - const info = stats.toJson(); - - if (stats.hasErrors()) { - console.error(info.errors); - process.exit(1); - } - - if (stats.hasWarnings()) { - console.warn(info.warnings); - process.exit(1); - } - runTests(); - } -); - -function runTests() { - try { - execSync('node ' + path.resolve(__dirname, 'dist', 'bundle.js'), { stdio: 'inherit' }); - } catch (_) { - process.exit(1); - } -} diff --git a/packages/node/test/manual/webpack-async-context/package.json b/packages/node/test/manual/webpack-async-context/package.json deleted file mode 100644 index 666406416c06..000000000000 --- a/packages/node/test/manual/webpack-async-context/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "webpack-async-context", - "version": "1.0.0", - "main": "index.js", - "license": "MIT", - "dependencies": { - "webpack": "^5.90.0" - }, - "volta": { - "extends": "../../../../../package.json" - } -} diff --git a/packages/node/test/manual/webpack-async-context/yarn.lock b/packages/node/test/manual/webpack-async-context/yarn.lock deleted file mode 100644 index 5ae121f60447..000000000000 --- a/packages/node/test/manual/webpack-async-context/yarn.lock +++ /dev/null @@ -1,550 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@jridgewell/gen-mapping@^0.3.0": - version "0.3.3" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" - integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== - dependencies: - "@jridgewell/set-array" "^1.0.1" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.9" - -"@jridgewell/resolve-uri@^3.1.0": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" - integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== - -"@jridgewell/set-array@^1.0.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" - integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== - -"@jridgewell/source-map@^0.3.3": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.5.tgz#a3bb4d5c6825aab0d281268f47f6ad5853431e91" - integrity sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ== - dependencies: - "@jridgewell/gen-mapping" "^0.3.0" - "@jridgewell/trace-mapping" "^0.3.9" - -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": - version "1.4.15" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" - integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== - -"@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.22" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz#72a621e5de59f5f1ef792d0793a82ee20f645e4c" - integrity sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw== - dependencies: - "@jridgewell/resolve-uri" "^3.1.0" - "@jridgewell/sourcemap-codec" "^1.4.14" - -"@types/eslint-scope@^3.7.3": - version "3.7.7" - resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" - integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== - dependencies: - "@types/eslint" "*" - "@types/estree" "*" - -"@types/eslint@*": - version "8.56.2" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.2.tgz#1c72a9b794aa26a8b94ad26d5b9aa51c8a6384bb" - integrity sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw== - dependencies: - "@types/estree" "*" - "@types/json-schema" "*" - -"@types/estree@*", "@types/estree@^1.0.5": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" - integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== - -"@types/json-schema@*", "@types/json-schema@^7.0.8": - version "7.0.15" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" - integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== - -"@types/node@*": - version "20.11.10" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.10.tgz#6c3de8974d65c362f82ee29db6b5adf4205462f9" - integrity sha512-rZEfe/hJSGYmdfX9tvcPMYeYPW2sNl50nsw4jZmRcaG0HIAb0WYEpsB05GOb53vjqpyE9GUhlDQ4jLSoB5q9kg== - dependencies: - undici-types "~5.26.4" - -"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" - integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q== - dependencies: - "@webassemblyjs/helper-numbers" "1.11.6" - "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - -"@webassemblyjs/floating-point-hex-parser@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" - integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== - -"@webassemblyjs/helper-api-error@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" - integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== - -"@webassemblyjs/helper-buffer@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093" - integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA== - -"@webassemblyjs/helper-numbers@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" - integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== - dependencies: - "@webassemblyjs/floating-point-hex-parser" "1.11.6" - "@webassemblyjs/helper-api-error" "1.11.6" - "@xtuc/long" "4.2.2" - -"@webassemblyjs/helper-wasm-bytecode@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" - integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== - -"@webassemblyjs/helper-wasm-section@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577" - integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g== - dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-buffer" "1.11.6" - "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/wasm-gen" "1.11.6" - -"@webassemblyjs/ieee754@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" - integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== - dependencies: - "@xtuc/ieee754" "^1.2.0" - -"@webassemblyjs/leb128@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" - integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== - dependencies: - "@xtuc/long" "4.2.2" - -"@webassemblyjs/utf8@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" - integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== - -"@webassemblyjs/wasm-edit@^1.11.5": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab" - integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw== - dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-buffer" "1.11.6" - "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/helper-wasm-section" "1.11.6" - "@webassemblyjs/wasm-gen" "1.11.6" - "@webassemblyjs/wasm-opt" "1.11.6" - "@webassemblyjs/wasm-parser" "1.11.6" - "@webassemblyjs/wast-printer" "1.11.6" - -"@webassemblyjs/wasm-gen@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268" - integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA== - dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/ieee754" "1.11.6" - "@webassemblyjs/leb128" "1.11.6" - "@webassemblyjs/utf8" "1.11.6" - -"@webassemblyjs/wasm-opt@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2" - integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g== - dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-buffer" "1.11.6" - "@webassemblyjs/wasm-gen" "1.11.6" - "@webassemblyjs/wasm-parser" "1.11.6" - -"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1" - integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ== - dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-api-error" "1.11.6" - "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/ieee754" "1.11.6" - "@webassemblyjs/leb128" "1.11.6" - "@webassemblyjs/utf8" "1.11.6" - -"@webassemblyjs/wast-printer@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20" - integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A== - dependencies: - "@webassemblyjs/ast" "1.11.6" - "@xtuc/long" "4.2.2" - -"@xtuc/ieee754@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" - integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== - -"@xtuc/long@4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" - integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== - -acorn-import-assertions@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" - integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== - -acorn@^8.7.1, acorn@^8.8.2: - version "8.11.3" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" - integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== - -ajv-keywords@^3.5.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" - integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== - -ajv@^6.12.5: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -browserslist@^4.21.10: - version "4.22.3" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.3.tgz#299d11b7e947a6b843981392721169e27d60c5a6" - integrity sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A== - dependencies: - caniuse-lite "^1.0.30001580" - electron-to-chromium "^1.4.648" - node-releases "^2.0.14" - update-browserslist-db "^1.0.13" - -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - -caniuse-lite@^1.0.30001580: - version "1.0.30001581" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001581.tgz#0dfd4db9e94edbdca67d57348ebc070dece279f4" - integrity sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ== - -chrome-trace-event@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4" - integrity sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ== - dependencies: - tslib "^1.9.0" - -commander@^2.20.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - -electron-to-chromium@^1.4.648: - version "1.4.648" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.648.tgz#c7b46c9010752c37bb4322739d6d2dd82354fbe4" - integrity sha512-EmFMarXeqJp9cUKu/QEciEApn0S/xRcpZWuAm32U7NgoZCimjsilKXHRO9saeEW55eHZagIDg6XTUOv32w9pjg== - -enhanced-resolve@^5.15.0: - version "5.15.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" - integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== - dependencies: - graceful-fs "^4.2.4" - tapable "^2.2.0" - -es-module-lexer@^1.2.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.4.1.tgz#41ea21b43908fe6a287ffcbe4300f790555331f5" - integrity sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w== - -escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== - -eslint-scope@5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" - integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== - dependencies: - esrecurse "^4.3.0" - estraverse "^4.1.1" - -esrecurse@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -estraverse@^5.2.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" - integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== - -events@^3.2.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" - integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== - -fast-deep-equal@^3.1.1: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -glob-to-regexp@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" - integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== - -graceful-fs@^4.1.2: - version "4.2.3" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" - integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== - -graceful-fs@^4.2.4, graceful-fs@^4.2.9: - version "4.2.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" - integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -jest-worker@^27.4.5: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" - integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== - dependencies: - "@types/node" "*" - merge-stream "^2.0.0" - supports-color "^8.0.0" - -json-parse-even-better-errors@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" - integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -loader-runner@^4.2.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" - integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== - -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@^2.1.27: - version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -neo-async@^2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" - integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== - -node-releases@^2.0.14: - version "2.0.14" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" - integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== - -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== - -punycode@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -randombytes@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -safe-buffer@^5.1.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" - integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== - -schema-utils@^3.1.1, schema-utils@^3.2.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" - integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== - dependencies: - "@types/json-schema" "^7.0.8" - ajv "^6.12.5" - ajv-keywords "^3.5.2" - -serialize-javascript@^6.0.1: - version "6.0.2" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" - integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== - dependencies: - randombytes "^2.1.0" - -source-map-support@~0.5.20: - version "0.5.21" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@^0.6.0: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -supports-color@^8.0.0: - version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - -tapable@^2.1.1, tapable@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" - integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== - -terser-webpack-plugin@^5.3.10: - version "5.3.10" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" - integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== - dependencies: - "@jridgewell/trace-mapping" "^0.3.20" - jest-worker "^27.4.5" - schema-utils "^3.1.1" - serialize-javascript "^6.0.1" - terser "^5.26.0" - -terser@^5.26.0: - version "5.27.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.27.0.tgz#70108689d9ab25fef61c4e93e808e9fd092bf20c" - integrity sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A== - dependencies: - "@jridgewell/source-map" "^0.3.3" - acorn "^8.8.2" - commander "^2.20.0" - source-map-support "~0.5.20" - -tslib@^1.9.0: - version "1.11.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35" - integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA== - -undici-types@~5.26.4: - version "5.26.5" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" - integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== - -update-browserslist-db@^1.0.13: - version "1.0.13" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" - integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg== - dependencies: - escalade "^3.1.1" - picocolors "^1.0.0" - -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - -watchpack@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" - integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== - dependencies: - glob-to-regexp "^0.4.1" - graceful-fs "^4.1.2" - -webpack-sources@^3.2.3: - version "3.2.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" - integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== - -webpack@^5.90.0: - version "5.90.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.90.0.tgz#313bfe16080d8b2fee6e29b6c986c0714ad4290e" - integrity sha512-bdmyXRCXeeNIePv6R6tGPyy20aUobw4Zy8r0LUS2EWO+U+Ke/gYDgsCh7bl5rB6jPpr4r0SZa6dPxBxLooDT3w== - dependencies: - "@types/eslint-scope" "^3.7.3" - "@types/estree" "^1.0.5" - "@webassemblyjs/ast" "^1.11.5" - "@webassemblyjs/wasm-edit" "^1.11.5" - "@webassemblyjs/wasm-parser" "^1.11.5" - acorn "^8.7.1" - acorn-import-assertions "^1.9.0" - browserslist "^4.21.10" - chrome-trace-event "^1.0.2" - enhanced-resolve "^5.15.0" - es-module-lexer "^1.2.1" - eslint-scope "5.1.1" - events "^3.2.0" - glob-to-regexp "^0.4.1" - graceful-fs "^4.2.9" - json-parse-even-better-errors "^2.3.1" - loader-runner "^4.2.0" - mime-types "^2.1.27" - neo-async "^2.6.2" - schema-utils "^3.2.0" - tapable "^2.1.1" - terser-webpack-plugin "^5.3.10" - watchpack "^2.4.0" - webpack-sources "^3.2.3" diff --git a/packages/node/test/module.test.ts b/packages/node/test/module.test.ts deleted file mode 100644 index cdf97834431e..000000000000 --- a/packages/node/test/module.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { createGetModuleFromFilename } from '../src/module'; - -const getModuleFromFilenameWindows = createGetModuleFromFilename('C:\\Users\\Tim', true); -const getModuleFromFilenamePosix = createGetModuleFromFilename('/Users/Tim'); - -describe('createGetModuleFromFilename', () => { - test('Windows', () => { - expect(getModuleFromFilenameWindows('C:\\Users\\Tim\\node_modules\\some-dep\\module.js')).toEqual( - 'some-dep:module', - ); - expect(getModuleFromFilenameWindows('C:\\Users\\Tim\\some\\more\\feature.js')).toEqual('some.more:feature'); - }); - - test('POSIX', () => { - expect(getModuleFromFilenamePosix('/Users/Tim/node_modules/some-dep/module.js')).toEqual('some-dep:module'); - expect(getModuleFromFilenamePosix('/Users/Tim/some/more/feature.js')).toEqual('some.more:feature'); - expect(getModuleFromFilenamePosix('/Users/Tim/main.js')).toEqual('main'); - }); - - test('.mjs', () => { - expect(getModuleFromFilenamePosix('/Users/Tim/node_modules/some-dep/module.mjs')).toEqual('some-dep:module'); - }); - - test('.cjs', () => { - expect(getModuleFromFilenamePosix('/Users/Tim/node_modules/some-dep/module.cjs')).toEqual('some-dep:module'); - }); - - test('node internal', () => { - expect(getModuleFromFilenamePosix('node.js')).toEqual('node'); - expect(getModuleFromFilenamePosix('node:internal/process/task_queues')).toEqual('task_queues'); - expect(getModuleFromFilenamePosix('node:internal/timers')).toEqual('timers'); - }); -}); diff --git a/packages/node/test/onuncaughtexception.test.ts b/packages/node/test/onuncaughtexception.test.ts deleted file mode 100644 index c06a3cb43a69..000000000000 --- a/packages/node/test/onuncaughtexception.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import * as SentryCore from '@sentry/core'; -import type { NodeClient } from '../src/client'; - -import { makeErrorHandler, onUncaughtExceptionIntegration } from '../src/integrations/onuncaughtexception'; - -const client = { - getOptions: () => ({}), - close: () => Promise.resolve(true), -} as unknown as NodeClient; - -jest.mock('@sentry/core', () => { - // we just want to short-circuit it, so dont worry about types - const original = jest.requireActual('@sentry/core'); - return { - ...original, - getClient: () => client, - }; -}); - -describe('uncaught exceptions', () => { - test('install global listener', () => { - const integration = onUncaughtExceptionIntegration(); - integration.setup!(client); - expect(process.listeners('uncaughtException')).toHaveLength(1); - }); - - test('makeErrorHandler', () => { - const captureExceptionMock = jest.spyOn(SentryCore, 'captureException'); - const handler = makeErrorHandler(client, { - exitEvenIfOtherHandlersAreRegistered: true, - onFatalError: () => {}, - }); - - handler({ message: 'message', name: 'name' }); - - expect(captureExceptionMock.mock.calls[0][1]).toEqual({ - originalException: { - message: 'message', - name: 'name', - }, - captureContext: { - level: 'fatal', - }, - mechanism: { - handled: false, - type: 'onuncaughtexception', - }, - }); - }); -}); diff --git a/packages/node/test/onunhandledrejection.test.ts b/packages/node/test/onunhandledrejection.test.ts deleted file mode 100644 index f20e7bd0274d..000000000000 --- a/packages/node/test/onunhandledrejection.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import * as SentryCore from '@sentry/core'; -import type { Client } from '@sentry/types'; - -import { makeUnhandledPromiseHandler, onUnhandledRejectionIntegration } from '../src/integrations/onunhandledrejection'; - -// don't log the test errors we're going to throw, so at a quick glance it doesn't look like the test itself has failed -global.console.warn = () => null; -global.console.error = () => null; - -describe('unhandled promises', () => { - test('install global listener', () => { - const client = { getOptions: () => ({}) } as unknown as Client; - SentryCore.setCurrentClient(client); - - const integration = onUnhandledRejectionIntegration(); - integration.setup!(client); - expect(process.listeners('unhandledRejection')).toHaveLength(1); - }); - - test('makeUnhandledPromiseHandler', () => { - const client = { getOptions: () => ({}) } as unknown as Client; - SentryCore.setCurrentClient(client); - - const promise = { - domain: { - sentryContext: { - extra: { extra: '1' }, - tags: { tag: '2' }, - user: { id: 1 }, - }, - }, - }; - - const captureException = jest.spyOn(SentryCore, 'captureException').mockImplementation(() => 'test'); - - const handler = makeUnhandledPromiseHandler(client, { - mode: 'warn', - }); - - handler('bla', promise); - - expect(captureException).toHaveBeenCalledWith('bla', { - originalException: { - domain: { - sentryContext: { - extra: { - extra: '1', - }, - tags: { - tag: '2', - }, - user: { - id: 1, - }, - }, - }, - }, - captureContext: { - extra: { - unhandledPromiseRejection: true, - }, - }, - mechanism: { - handled: false, - type: 'onunhandledrejection', - }, - }); - expect(captureException.mock.calls[0][0]).toBe('bla'); - }); -}); diff --git a/packages/node/test/performance.test.ts b/packages/node/test/performance.test.ts deleted file mode 100644 index b8a0d34cb054..000000000000 --- a/packages/node/test/performance.test.ts +++ /dev/null @@ -1,245 +0,0 @@ -import { - setAsyncContextStrategy, - setCurrentClient, - startInactiveSpan, - startSpan, - startSpanManual, - withIsolationScope, - withScope, -} from '@sentry/core'; -import type { Span, TransactionEvent } from '@sentry/types'; -import { NodeClient, defaultStackParser } from '../src'; -import { setNodeAsyncContextStrategy } from '../src/async'; -import { getDefaultNodeClientOptions } from './helper/node-client-options'; - -const dsn = 'https://53039209a22b4ec1bcc296a3c9fdecd6@sentry.io/4291'; - -beforeAll(() => { - setNodeAsyncContextStrategy(); -}); - -afterAll(() => { - setAsyncContextStrategy(undefined); -}); - -describe('startSpan()', () => { - it('should correctly separate spans when called after one another with interwoven timings', async () => { - const transactionEventPromise = new Promise(resolve => { - setCurrentClient( - new NodeClient( - getDefaultNodeClientOptions({ - stackParser: defaultStackParser, - tracesSampleRate: 1, - beforeSendTransaction: event => { - resolve(event); - return null; - }, - dsn, - }), - ), - ); - }); - - startSpan({ name: 'first' }, () => { - return new Promise(resolve => { - setTimeout(resolve, 500); - }); - }); - - startSpan({ name: 'second' }, () => { - return new Promise(resolve => { - setTimeout(resolve, 250); - }); - }); - - const transactionEvent = await transactionEventPromise; - - // Any transaction events happening shouldn't have any child spans - expect(transactionEvent.spans).toStrictEqual([]); - }); - - it('should correctly nest spans when called within one another', async () => { - const transactionEventPromise = new Promise(resolve => { - setCurrentClient( - new NodeClient( - getDefaultNodeClientOptions({ - stackParser: defaultStackParser, - tracesSampleRate: 1, - beforeSendTransaction: event => { - resolve(event); - return null; - }, - dsn, - }), - ), - ); - }); - - startSpan({ name: 'first' }, () => { - startSpan({ name: 'second' }, () => undefined); - }); - - const transactionEvent = await transactionEventPromise; - - expect(transactionEvent.spans).toHaveLength(1); - expect(transactionEvent.spans?.[0].description).toBe('second'); - }); -}); - -describe('startSpanManual()', () => { - it('should correctly separate spans when called after one another with interwoven timings', async () => { - const transactionEventPromise = new Promise(resolve => { - setCurrentClient( - new NodeClient( - getDefaultNodeClientOptions({ - stackParser: defaultStackParser, - tracesSampleRate: 1, - beforeSendTransaction: event => { - resolve(event); - return null; - }, - dsn, - }), - ), - ); - }); - - startSpanManual({ name: 'first' }, span => { - return new Promise(resolve => { - setTimeout(() => { - span.end(); - resolve(); - }, 500); - }); - }); - - startSpanManual({ name: 'second' }, span => { - return new Promise(resolve => { - setTimeout(() => { - span.end(); - resolve(); - }, 500); - }); - }); - - const transactionEvent = await transactionEventPromise; - - // Any transaction events happening shouldn't have any child spans - expect(transactionEvent.spans).toStrictEqual([]); - }); - - it('should correctly nest spans when called within one another', async () => { - const transactionEventPromise = new Promise(resolve => { - setCurrentClient( - new NodeClient( - getDefaultNodeClientOptions({ - stackParser: defaultStackParser, - tracesSampleRate: 1, - beforeSendTransaction: event => { - resolve(event); - return null; - }, - dsn, - }), - ), - ); - }); - - startSpanManual({ name: 'first' }, span1 => { - startSpanManual({ name: 'second' }, span2 => { - span2?.end(); - }); - span1?.end(); - }); - - const transactionEvent = await transactionEventPromise; - - expect(transactionEvent.spans?.[0].description).toBe('second'); - }); - - it('should use the scopes at time of creation instead of the scopes at time of termination', async () => { - const transactionEventPromise = new Promise(resolve => { - setCurrentClient( - new NodeClient( - getDefaultNodeClientOptions({ - stackParser: defaultStackParser, - tracesSampleRate: 1, - beforeSendTransaction: event => { - resolve(event); - return null; - }, - dsn, - }), - ), - ); - }); - - withIsolationScope(isolationScope1 => { - isolationScope1.setTag('isolationScope', 1); - withScope(scope1 => { - scope1.setTag('scope', 1); - startSpanManual({ name: 'my-span' }, span => { - withIsolationScope(isolationScope2 => { - isolationScope2.setTag('isolationScope', 2); - withScope(scope2 => { - scope2.setTag('scope', 2); - span.end(); - }); - }); - }); - }); - }); - - expect(await transactionEventPromise).toMatchObject({ - tags: { - scope: 1, - isolationScope: 1, - }, - }); - }); -}); - -describe('startInactiveSpan()', () => { - it('should use the scopes at time of creation instead of the scopes at time of termination', async () => { - const transactionEventPromise = new Promise(resolve => { - setCurrentClient( - new NodeClient( - getDefaultNodeClientOptions({ - stackParser: defaultStackParser, - tracesSampleRate: 1, - beforeSendTransaction: event => { - resolve(event); - return null; - }, - dsn, - }), - ), - ); - }); - - let span: Span | undefined; - - withIsolationScope(isolationScope => { - isolationScope.setTag('isolationScope', 1); - withScope(scope => { - scope.setTag('scope', 1); - span = startInactiveSpan({ name: 'my-span' }); - }); - }); - - withIsolationScope(isolationScope => { - isolationScope.setTag('isolationScope', 2); - withScope(scope => { - scope.setTag('scope', 2); - span?.end(); - }); - }); - - expect(await transactionEventPromise).toMatchObject({ - tags: { - scope: 1, - isolationScope: 1, - }, - }); - }); -}); diff --git a/packages/node/test/sdk.test.ts b/packages/node/test/sdk.test.ts deleted file mode 100644 index e29b7d2a7c31..000000000000 --- a/packages/node/test/sdk.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { Integration } from '@sentry/types'; - -import * as SentryCore from '@sentry/core'; -import { init } from '../src/sdk'; - -// eslint-disable-next-line no-var -declare var global: any; - -const PUBLIC_DSN = 'https://username@domain/123'; - -class MockIntegration implements Integration { - public name: string; - public setupOnce: jest.Mock = jest.fn(); - public constructor(name: string) { - this.name = name; - } -} - -describe('init()', () => { - const initAndBindSpy = jest.spyOn(SentryCore, 'initAndBind'); - - beforeEach(() => { - global.__SENTRY__ = {}; - }); - - it("doesn't install default integrations if told not to", () => { - init({ dsn: PUBLIC_DSN, defaultIntegrations: false }); - - expect(initAndBindSpy).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - integrations: [], - }), - ); - }); - - it('installs merged default integrations, with overrides provided through options', () => { - const mockDefaultIntegrations = [ - new MockIntegration('Some mock integration 2.1'), - new MockIntegration('Some mock integration 2.2'), - ]; - - const mockIntegrations = [ - new MockIntegration('Some mock integration 2.1'), - new MockIntegration('Some mock integration 2.3'), - ]; - - init({ dsn: PUBLIC_DSN, integrations: mockIntegrations, defaultIntegrations: mockDefaultIntegrations }); - - expect(mockDefaultIntegrations[0].setupOnce as jest.Mock).toHaveBeenCalledTimes(0); - expect(mockDefaultIntegrations[1].setupOnce as jest.Mock).toHaveBeenCalledTimes(1); - expect(mockIntegrations[0].setupOnce as jest.Mock).toHaveBeenCalledTimes(1); - expect(mockIntegrations[1].setupOnce as jest.Mock).toHaveBeenCalledTimes(1); - }); - - it('installs integrations returned from a callback function', () => { - const mockDefaultIntegrations = [ - new MockIntegration('Some mock integration 3.1'), - new MockIntegration('Some mock integration 3.2'), - ]; - - const newIntegration = new MockIntegration('Some mock integration 3.3'); - - init({ - dsn: PUBLIC_DSN, - defaultIntegrations: mockDefaultIntegrations, - integrations: integrations => { - const newIntegrations = [...integrations]; - newIntegrations[1] = newIntegration; - return newIntegrations; - }, - }); - - expect(mockDefaultIntegrations[0].setupOnce as jest.Mock).toHaveBeenCalledTimes(1); - expect(mockDefaultIntegrations[1].setupOnce as jest.Mock).toHaveBeenCalledTimes(0); - expect(newIntegration.setupOnce as jest.Mock).toHaveBeenCalledTimes(1); - }); -}); diff --git a/packages/node-experimental/test/sdk/api.test.ts b/packages/node/test/sdk/api.test.ts similarity index 100% rename from packages/node-experimental/test/sdk/api.test.ts rename to packages/node/test/sdk/api.test.ts diff --git a/packages/node-experimental/test/sdk/client.test.ts b/packages/node/test/sdk/client.test.ts similarity index 100% rename from packages/node-experimental/test/sdk/client.test.ts rename to packages/node/test/sdk/client.test.ts diff --git a/packages/node-experimental/test/sdk/init.test.ts b/packages/node/test/sdk/init.test.ts similarity index 100% rename from packages/node-experimental/test/sdk/init.test.ts rename to packages/node/test/sdk/init.test.ts diff --git a/packages/node-experimental/test/sdk/scope.test.ts b/packages/node/test/sdk/scope.test.ts similarity index 100% rename from packages/node-experimental/test/sdk/scope.test.ts rename to packages/node/test/sdk/scope.test.ts diff --git a/packages/node/test/stacktrace.test.ts b/packages/node/test/stacktrace.test.ts deleted file mode 100644 index 968cdccca9a4..000000000000 --- a/packages/node/test/stacktrace.test.ts +++ /dev/null @@ -1,442 +0,0 @@ -/** - * stack-trace - Parses node.js stack traces - * - * These tests were originally forked to fix this issue: - * https://github.com/felixge/node-stack-trace/issues/31 - * - * Mar 19,2019 - #4fd379e - * - * https://github.com/felixge/node-stack-trace/ - * @license MIT - */ - -import { parseStackFrames } from '@sentry/utils'; - -import { defaultStackParser as stackParser } from '../src/sdk'; - -function testBasic() { - return new Error('something went wrong'); -} - -function testWrapper() { - return testBasic(); -} - -function evalWrapper() { - return eval('testWrapper()'); -} - -describe('Stack parsing', () => { - test('test basic error', () => { - const frames = parseStackFrames(stackParser, testBasic()); - - const last = frames.length - 1; - expect(frames[last].filename).toEqual(__filename); - expect(frames[last].function).toEqual('testBasic'); - expect(frames[last].lineno).toEqual(18); - expect(frames[last].colno).toEqual(10); - }); - - test('test error with wrapper', () => { - const frames = parseStackFrames(stackParser, testWrapper()); - - const last = frames.length - 1; - expect(frames[last].function).toEqual('testBasic'); - expect(frames[last - 1].function).toEqual('testWrapper'); - }); - - test('test error with eval wrapper', () => { - const frames = parseStackFrames(stackParser, evalWrapper()); - - const last = frames.length - 1; - expect(frames[last].function).toEqual('testBasic'); - expect(frames[last - 1].function).toEqual('testWrapper'); - expect(frames[last - 2].function).toEqual('eval'); - }); - - test('parses object in fn name', () => { - const err = new Error(); - err.stack = - 'Error: Foo\n' + - ' at [object Object].global.every [as _onTimeout] (/Users/hoitz/develop/test.coffee:36:3)\n' + - ' at Timer.listOnTimeout [as ontimeout] (timers.js:110:15)\n'; - - const frames = parseStackFrames(stackParser, err); - - expect(frames).toEqual([ - { - filename: 'timers.js', - module: 'timers', - function: 'Timer.listOnTimeout [as ontimeout]', - lineno: 110, - colno: 15, - in_app: false, - }, - { - filename: '/Users/hoitz/develop/test.coffee', - module: 'test.coffee', - function: '[object Object].global.every [as _onTimeout]', - lineno: 36, - colno: 3, - in_app: true, - }, - ]); - }); - - test('parses undefined stack', () => { - const err = { stack: undefined }; - const trace = parseStackFrames(stackParser, err as Error); - - expect(trace).toEqual([]); - }); - - test('parses corrupt stack', () => { - const err = new Error(); - err.stack = - 'AssertionError: true == false\n' + - ' fuck' + - ' at Test.run (/Users/felix/code/node-fast-or-slow/lib/test.js:45:10)\n' + - 'oh no' + - ' at TestCase.run (/Users/felix/code/node-fast-or-slow/lib/test_case.js:61:8)\n'; - - const frames = parseStackFrames(stackParser, err); - - expect(frames).toEqual([ - { - filename: '/Users/felix/code/node-fast-or-slow/lib/test_case.js', - module: 'test_case', - function: 'TestCase.run', - lineno: 61, - colno: 8, - in_app: true, - }, - { - filename: '/Users/felix/code/node-fast-or-slow/lib/test.js', - module: 'test', - function: 'Test.run', - lineno: 45, - colno: 10, - in_app: true, - }, - ]); - }); - - test('parses with native methods', () => { - const err = new Error(); - err.stack = - 'AssertionError: true == false\n' + - ' at Test.fn (/Users/felix/code/node-fast-or-slow/test/fast/example/test-example.js:6:10)\n' + - ' at Test.run (/Users/felix/code/node-fast-or-slow/lib/test.js:45:10)\n' + - ' at TestCase.runNext (/Users/felix/code/node-fast-or-slow/lib/test_case.js:73:8)\n' + - ' at TestCase.run (/Users/felix/code/node-fast-or-slow/lib/test_case.js:61:8)\n' + - ' at Array.0 (native)\n' + - ' at EventEmitter._tickCallback (node.js:126:26)'; - - const frames = parseStackFrames(stackParser, err); - - expect(frames).toEqual([ - { - filename: 'node.js', - module: 'node', - function: 'EventEmitter._tickCallback', - lineno: 126, - colno: 26, - in_app: false, - }, - { - filename: '/Users/felix/code/node-fast-or-slow/test/fast/example/test-example.js', - function: 'Array.0', - in_app: false, - }, - { - filename: '/Users/felix/code/node-fast-or-slow/lib/test_case.js', - module: 'test_case', - function: 'TestCase.run', - lineno: 61, - colno: 8, - in_app: true, - }, - { - filename: '/Users/felix/code/node-fast-or-slow/lib/test_case.js', - module: 'test_case', - function: 'TestCase.runNext', - lineno: 73, - colno: 8, - in_app: true, - }, - { - filename: '/Users/felix/code/node-fast-or-slow/lib/test.js', - module: 'test', - function: 'Test.run', - lineno: 45, - colno: 10, - in_app: true, - }, - { - filename: '/Users/felix/code/node-fast-or-slow/test/fast/example/test-example.js', - module: 'test-example', - function: 'Test.fn', - lineno: 6, - colno: 10, - in_app: true, - }, - ]); - }); - - test('parses with file only', () => { - const err = new Error(); - err.stack = 'AssertionError: true == false\n' + ' at /Users/felix/code/node-fast-or-slow/lib/test_case.js:80:10'; - - const frames = parseStackFrames(stackParser, err); - - expect(frames).toEqual([ - { - filename: '/Users/felix/code/node-fast-or-slow/lib/test_case.js', - module: 'test_case', - function: '?', - lineno: 80, - colno: 10, - in_app: true, - }, - ]); - }); - - test('parses with multi line message', () => { - const err = new Error(); - err.stack = - 'AssertionError: true == false\nAnd some more shit\n' + - ' at /Users/felix/code/node-fast-or-slow/lib/test_case.js:80:10'; - - const frames = parseStackFrames(stackParser, err); - - expect(frames).toEqual([ - { - filename: '/Users/felix/code/node-fast-or-slow/lib/test_case.js', - module: 'test_case', - function: '?', - lineno: 80, - colno: 10, - in_app: true, - }, - ]); - }); - - test('parses with anonymous fn call', () => { - const err = new Error(); - err.stack = - 'AssertionError: expected [] to be arguments\n' + - ' at Assertion.prop.(anonymous function) (/Users/den/Projects/should.js/lib/should.js:60:14)\n'; - - const frames = parseStackFrames(stackParser, err); - - expect(frames).toEqual([ - { - filename: '/Users/den/Projects/should.js/lib/should.js', - module: 'should', - function: 'Assertion.prop.(anonymous function)', - lineno: 60, - colno: 14, - in_app: true, - }, - ]); - }); - - test('parses with braces in paths', () => { - const err = new Error(); - err.stack = - 'AssertionError: true == false\n' + - ' at Test.run (/Users/felix (something)/code/node-fast-or-slow/lib/test.js:45:10)\n' + - ' at TestCase.run (/Users/felix (something)/code/node-fast-or-slow/lib/test_case.js:61:8)\n'; - - const frames = parseStackFrames(stackParser, err); - - expect(frames).toEqual([ - { - filename: '/Users/felix (something)/code/node-fast-or-slow/lib/test_case.js', - module: 'test_case', - function: 'TestCase.run', - lineno: 61, - colno: 8, - in_app: true, - }, - { - filename: '/Users/felix (something)/code/node-fast-or-slow/lib/test.js', - module: 'test', - function: 'Test.run', - lineno: 45, - colno: 10, - in_app: true, - }, - ]); - }); - - test('parses with async frames', () => { - // https://github.com/getsentry/sentry-javascript/issues/4692#issuecomment-1063835795 - const err = new Error(); - err.stack = - 'Error: Client request error\n' + - ' at Object.httpRequestError (file:///code/node_modules/@waroncancer/gaia/lib/error/error-factory.js:17:73)\n' + - ' at Object.run (file:///code/node_modules/@waroncancer/gaia/lib/http-client/http-client.js:81:36)\n' + - ' at processTicksAndRejections (node:internal/process/task_queues:96:5)\n' + - ' at async Object.send (file:///code/lib/post-created/send-post-created-notification-module.js:17:27)\n' + - ' at async each (file:///code/lib/process-post-events-module.js:14:21)\n' + - ' at async Runner.processEachMessage (/code/node_modules/kafkajs/src/consumer/runner.js:151:9)\n' + - ' at async onBatch (/code/node_modules/kafkajs/src/consumer/runner.js:326:9)\n' + - ' at async /code/node_modules/kafkajs/src/consumer/runner.js:376:15\n'; - - const frames = parseStackFrames(stackParser, err); - - expect(frames).toEqual([ - { - filename: '/code/node_modules/kafkajs/src/consumer/runner.js', - module: 'kafkajs.src.consumer:runner', - function: '?', - lineno: 376, - colno: 15, - in_app: false, - }, - { - filename: '/code/node_modules/kafkajs/src/consumer/runner.js', - module: 'kafkajs.src.consumer:runner', - function: 'onBatch', - lineno: 326, - colno: 9, - in_app: false, - }, - { - filename: '/code/node_modules/kafkajs/src/consumer/runner.js', - module: 'kafkajs.src.consumer:runner', - function: 'Runner.processEachMessage', - lineno: 151, - colno: 9, - in_app: false, - }, - { - filename: '/code/lib/process-post-events-module.js', - module: 'process-post-events-module', - function: 'each', - lineno: 14, - colno: 21, - in_app: true, - }, - { - filename: '/code/lib/post-created/send-post-created-notification-module.js', - module: 'send-post-created-notification-module', - function: 'Object.send', - lineno: 17, - colno: 27, - in_app: true, - }, - { - filename: 'node:internal/process/task_queues', - module: 'task_queues', - function: 'processTicksAndRejections', - lineno: 96, - colno: 5, - in_app: false, - }, - { - filename: '/code/node_modules/@waroncancer/gaia/lib/http-client/http-client.js', - module: '@waroncancer.gaia.lib.http-client:http-client', - function: 'Object.run', - lineno: 81, - colno: 36, - in_app: false, - }, - { - filename: '/code/node_modules/@waroncancer/gaia/lib/error/error-factory.js', - module: '@waroncancer.gaia.lib.error:error-factory', - function: 'Object.httpRequestError', - lineno: 17, - colno: 73, - in_app: false, - }, - ]); - }); - - test('parses with async frames Windows', () => { - // https://github.com/getsentry/sentry-javascript/issues/4692#issuecomment-1063835795 - const err = new Error(); - err.stack = - 'Error: Client request error\n' + - ' at Object.httpRequestError (file:///C:/code/node_modules/@waroncancer/gaia/lib/error/error-factory.js:17:73)\n' + - ' at Object.run (file:///C:/code/node_modules/@waroncancer/gaia/lib/http-client/http-client.js:81:36)\n' + - ' at processTicksAndRejections (node:internal/process/task_queues:96:5)\n' + - ' at async Object.send (file:///C:/code/lib/post-created/send-post-created-notification-module.js:17:27)\n' + - ' at async each (file:///C:/code/lib/process-post-events-module.js:14:21)\n'; - - const frames = parseStackFrames(stackParser, err); - - expect(frames).toEqual([ - { - filename: 'C:/code/lib/process-post-events-module.js', - module: 'process-post-events-module', - function: 'each', - lineno: 14, - colno: 21, - in_app: true, - }, - { - filename: 'C:/code/lib/post-created/send-post-created-notification-module.js', - module: 'send-post-created-notification-module', - function: 'Object.send', - lineno: 17, - colno: 27, - in_app: true, - }, - { - filename: 'node:internal/process/task_queues', - module: 'task_queues', - function: 'processTicksAndRejections', - lineno: 96, - colno: 5, - in_app: false, - }, - { - filename: 'C:/code/node_modules/@waroncancer/gaia/lib/http-client/http-client.js', - module: '@waroncancer.gaia.lib.http-client:http-client', - function: 'Object.run', - lineno: 81, - colno: 36, - in_app: false, - }, - { - filename: 'C:/code/node_modules/@waroncancer/gaia/lib/error/error-factory.js', - module: '@waroncancer.gaia.lib.error:error-factory', - function: 'Object.httpRequestError', - lineno: 17, - colno: 73, - in_app: false, - }, - ]); - }); - - test('parses with colons in paths', () => { - const err = new Error(); - err.stack = - 'AssertionError: true == false\n' + - ' at Test.run (/Users/felix/code/node-fast-or-slow/lib/20:20:20/test.js:45:10)\n' + - ' at TestCase.run (/Users/felix/code/node-fast-or-slow/lib/test_case.js:61:8)\n'; - - const frames = parseStackFrames(stackParser, err); - - expect(frames).toEqual([ - { - filename: '/Users/felix/code/node-fast-or-slow/lib/test_case.js', - module: 'test_case', - function: 'TestCase.run', - lineno: 61, - colno: 8, - in_app: true, - }, - { - filename: '/Users/felix/code/node-fast-or-slow/lib/20:20:20/test.js', - module: 'test', - function: 'Test.run', - lineno: 45, - colno: 10, - in_app: true, - }, - ]); - }); -}); diff --git a/packages/node/test/transports/http.test.ts b/packages/node/test/transports/http.test.ts index ddf73039a009..e945c086959a 100644 --- a/packages/node/test/transports/http.test.ts +++ b/packages/node/test/transports/http.test.ts @@ -1,6 +1,4 @@ -/* eslint-disable deprecation/deprecation */ import * as http from 'http'; - import { createGunzip } from 'zlib'; import { createTransport } from '@sentry/core'; import type { EventEnvelope, EventItem } from '@sentry/types'; @@ -51,19 +49,18 @@ function setupTestServer( res.end(); // also terminate socket because keepalive hangs connection a bit - if (res.connection) { - res.connection.end(); - } + // eslint-disable-next-line deprecation/deprecation + res.connection?.end(); }); - testServer.listen(18099); + testServer.listen(18101); return new Promise(resolve => { testServer?.on('listening', resolve); }); } -const TEST_SERVER_URL = 'http://localhost:18099'; +const TEST_SERVER_URL = 'http://localhost:18101'; const EVENT_ENVELOPE = createEnvelope({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [ [{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem, diff --git a/packages/node/test/transports/https.test.ts b/packages/node/test/transports/https.test.ts index a45319c40e42..8b0d3312ba54 100644 --- a/packages/node/test/transports/https.test.ts +++ b/packages/node/test/transports/https.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable deprecation/deprecation */ import * as http from 'http'; import * as https from 'https'; import { createTransport } from '@sentry/core'; @@ -50,19 +49,18 @@ function setupTestServer( res.end(); // also terminate socket because keepalive hangs connection a bit - if (res.connection) { - res.connection.end(); - } + // eslint-disable-next-line deprecation/deprecation + res.connection?.end(); }); - testServer.listen(8099); + testServer.listen(8100); return new Promise(resolve => { testServer?.on('listening', resolve); }); } -const TEST_SERVER_URL = 'https://localhost:8099'; +const TEST_SERVER_URL = 'https://localhost:8100'; const EVENT_ENVELOPE = createEnvelope({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [ [{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem, diff --git a/packages/node/test/utils.ts b/packages/node/test/utils.ts deleted file mode 100644 index 7487e0d49e32..000000000000 --- a/packages/node/test/utils.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { NODE_VERSION } from '../src/nodeVersion'; - -/** - * Returns`describe` or `describe.skip` depending on allowed major versions of Node. - * - * @param {{ min?: number; max?: number }} allowedVersion - * @return {*} {jest.Describe} - */ -export const conditionalTest = (allowedVersion: { min?: number; max?: number }): jest.Describe => { - const major = NODE_VERSION.major; - if (!major) { - return describe.skip as jest.Describe; - } - - return major < (allowedVersion.min || -Infinity) || major > (allowedVersion.max || Infinity) - ? (describe.skip as jest.Describe) - : (describe as any); -}; diff --git a/packages/node-experimental/test/utils/getRequestUrl.test.ts b/packages/node/test/utils/getRequestUrl.test.ts similarity index 100% rename from packages/node-experimental/test/utils/getRequestUrl.test.ts rename to packages/node/test/utils/getRequestUrl.test.ts diff --git a/packages/node/tsconfig.json b/packages/node/tsconfig.json index 89a9b9e0e2fe..8f38d240197e 100644 --- a/packages/node/tsconfig.json +++ b/packages/node/tsconfig.json @@ -4,6 +4,7 @@ "include": ["src/**/*"], "compilerOptions": { - "lib": ["es2018"] + "lib": ["es2018"], + "module": "Node16" } } diff --git a/packages/node/tsconfig.test.json b/packages/node/tsconfig.test.json index 52333183eb70..87f6afa06b86 100644 --- a/packages/node/tsconfig.test.json +++ b/packages/node/tsconfig.test.json @@ -1,7 +1,7 @@ { "extends": "./tsconfig.json", - "include": ["test/**/*", "src/**/*.d.ts"], + "include": ["test/**/*"], "compilerOptions": { // should include all types from `./tsconfig.json` plus types for all test frameworks used diff --git a/packages/opentelemetry/README.md b/packages/opentelemetry/README.md index 66fcb1a9e287..3a3058746701 100644 --- a/packages/opentelemetry/README.md +++ b/packages/opentelemetry/README.md @@ -86,7 +86,8 @@ function setupSentry() { } ``` -A full setup example can be found in [node-experimental](./../node-experimental). +A full setup example can be found in +[node-experimental](https://github.com/getsentry/sentry-javascript/blob/develop/packages/node-experimental). ## Links diff --git a/packages/opentelemetry/rollup.npm.config.mjs b/packages/opentelemetry/rollup.npm.config.mjs index fd61fbf7c62c..e015fea4935e 100644 --- a/packages/opentelemetry/rollup.npm.config.mjs +++ b/packages/opentelemetry/rollup.npm.config.mjs @@ -7,7 +7,10 @@ export default makeNPMConfigVariants( // set exports to 'named' or 'auto' so that rollup doesn't warn exports: 'named', // set preserveModules to false because we want to bundle everything into one file. - preserveModules: false, + preserveModules: + process.env.SENTRY_BUILD_PRESERVE_MODULES === undefined + ? false + : Boolean(process.env.SENTRY_BUILD_PRESERVE_MODULES), }, }, }), diff --git a/packages/opentelemetry/src/custom/getCurrentHub.ts b/packages/opentelemetry/src/custom/getCurrentHub.ts index 81ccc46b6ddb..9db09297d670 100644 --- a/packages/opentelemetry/src/custom/getCurrentHub.ts +++ b/packages/opentelemetry/src/custom/getCurrentHub.ts @@ -1,4 +1,4 @@ -import type { Client, EventHint, Hub, Integration, IntegrationClass, Scope, SeverityLevel } from '@sentry/types'; +import type { Client, EventHint, Hub, Integration, IntegrationClass, SeverityLevel } from '@sentry/types'; import { addBreadcrumb, @@ -23,25 +23,11 @@ import { */ export function getCurrentHub(): Hub { return { - isOlderThan(_version: number): boolean { - return false; - }, - bindClient(client: Client): void { const scope = getCurrentScope(); scope.setClient(client); }, - pushScope(): Scope { - // TODO: This does not work and is actually deprecated - return getCurrentScope(); - }, - - popScope(): boolean { - // TODO: This does not work and is actually deprecated - return false; - }, - withScope, getClient: () => getClient() as C | undefined, getScope: getCurrentScope, @@ -79,11 +65,6 @@ export function getCurrentHub(): Hub { // only send the update _sendSessionUpdate(); }, - - shouldSendDefaultPii(): boolean { - const client = getClient(); - return Boolean(client ? client.getOptions().sendDefaultPii : false); - }, }; } diff --git a/packages/opentelemetry/src/propagator.ts b/packages/opentelemetry/src/propagator.ts index b8de206d84f1..d96f19c16b7c 100644 --- a/packages/opentelemetry/src/propagator.ts +++ b/packages/opentelemetry/src/propagator.ts @@ -1,9 +1,10 @@ -import type { Baggage, Context, SpanContext, TextMapGetter, TextMapSetter } from '@opentelemetry/api'; +import type { Baggage, Context, Span, SpanContext, TextMapGetter, TextMapSetter } from '@opentelemetry/api'; 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 { getRootSpan } from '@sentry/core'; import { spanToJSON } from '@sentry/core'; import { getClient, getCurrentScope, getDynamicSamplingContextFromClient, getIsolationScope } from '@sentry/core'; import type { DynamicSamplingContext, Options, PropagationContext } from '@sentry/types'; @@ -14,6 +15,7 @@ import { dynamicSamplingContextToSentryBaggageHeader, generateSentryTraceHeader, logger, + parseBaggageHeader, propagationContextFromHeaders, stringMatchesSomePattern, } from '@sentry/utils'; @@ -27,18 +29,27 @@ import { } from './constants'; import { DEBUG_BUILD } from './debug-build'; import { getScopesFromContext, setScopesOnContext } from './utils/contextData'; +import { getDynamicSamplingContextFromSpan } from './utils/dynamicSamplingContext'; +import { getSamplingDecision } from './utils/getSamplingDecision'; import { setIsSetup } from './utils/setupCheck'; /** Get the Sentry propagation context from a span context. */ -export function getPropagationContextFromSpanContext(spanContext: SpanContext): PropagationContext { +export function getPropagationContextFromSpan(span: Span): PropagationContext { + const spanContext = span.spanContext(); const { traceId, spanId, traceState } = spanContext; + // When we have a dsc trace state, it means this came from the incoming trace + // Then this takes presedence over the root span const dscString = traceState ? traceState.get(SENTRY_TRACE_STATE_DSC) : undefined; - const dsc = dscString ? baggageHeaderToDynamicSamplingContext(dscString) : undefined; + const traceStateDsc = dscString ? baggageHeaderToDynamicSamplingContext(dscString) : undefined; + const parentSpanId = traceState ? traceState.get(SENTRY_TRACE_STATE_PARENT_SPAN_ID) : undefined; const sampled = getSamplingDecision(spanContext); + // No trace state? --> Take DSC from root span + const dsc = traceStateDsc || getDynamicSamplingContextFromSpan(getRootSpan(span)); + return { traceId, spanId, @@ -85,10 +96,21 @@ export class SentryPropagator extends W3CBaggagePropagator { return; } + const existingBaggageHeader = getExistingBaggage(carrier); let baggage = propagation.getBaggage(context) || propagation.createBaggage({}); const { dynamicSamplingContext, traceId, spanId, sampled } = getInjectionData(context); + if (existingBaggageHeader) { + const baggageEntries = parseBaggageHeader(existingBaggageHeader); + + if (baggageEntries) { + Object.entries(baggageEntries).forEach(([key, value]) => { + baggage = baggage.setEntry(key, { value }); + }); + } + } + if (dynamicSamplingContext) { baggage = Object.entries(dynamicSamplingContext).reduce((b, [dscKey, dscValue]) => { if (dscValue) { @@ -196,7 +218,8 @@ function getInjectionData(context: Context): { // If we have a local span, we can just pick everything from it if (span && !spanIsRemote) { const spanContext = span.spanContext(); - const propagationContext = getPropagationContextFromSpanContext(spanContext); + + const propagationContext = getPropagationContextFromSpan(span); const dynamicSamplingContext = getDynamicSamplingContext(propagationContext, spanContext.traceId); return { dynamicSamplingContext, @@ -220,9 +243,9 @@ function getInjectionData(context: Context): { } // Else, we look at the remote span context - const spanContext = trace.getSpanContext(context); - if (spanContext) { - const propagationContext = getPropagationContextFromSpanContext(spanContext); + if (span) { + const spanContext = span.spanContext(); + const propagationContext = getPropagationContextFromSpan(span); const dynamicSamplingContext = getDynamicSamplingContext(propagationContext, spanContext.traceId); return { @@ -301,40 +324,12 @@ export function continueTraceAsRemoteSpan( return context.with(ctxWithSpanContext, callback); } -/** - * OpenTelemetry only knows about SAMPLED or NONE decision, - * but for us it is important to differentiate between unset and unsampled. - * - * Both of these are identified as `traceFlags === TracegFlags.NONE`, - * but we additionally look at a special trace state to differentiate between them. - */ -export function getSamplingDecision(spanContext: SpanContext): boolean | undefined { - const { traceFlags, traceState } = spanContext; - - const sampledNotRecording = traceState ? traceState.get(SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING) === '1' : false; - - // If trace flag is `SAMPLED`, we interpret this as sampled - // If it is `NONE`, it could mean either it was sampled to be not recorder, or that it was not sampled at all - // For us this is an important difference, sow e look at the SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING - // to identify which it is - if (traceFlags === TraceFlags.SAMPLED) { - return true; - } - - if (sampledNotRecording) { - return false; - } - - // Fall back to DSC as a last resort, that may also contain `sampled`... - const dscString = traceState ? traceState.get(SENTRY_TRACE_STATE_DSC) : undefined; - const dsc = dscString ? baggageHeaderToDynamicSamplingContext(dscString) : undefined; - - if (dsc?.sampled === 'true') { - return true; - } - if (dsc?.sampled === 'false') { - return false; +/** Try to get the existing baggage header so we can merge this in. */ +function getExistingBaggage(carrier: unknown): string | undefined { + try { + const baggage = (carrier as Record)[SENTRY_BAGGAGE_HEADER]; + return Array.isArray(baggage) ? baggage.join(',') : baggage; + } catch { + return undefined; } - - return undefined; } diff --git a/packages/opentelemetry/src/sampler.ts b/packages/opentelemetry/src/sampler.ts index 9ae0b60699ca..e611d6a43395 100644 --- a/packages/opentelemetry/src/sampler.ts +++ b/packages/opentelemetry/src/sampler.ts @@ -1,19 +1,21 @@ -import type { Attributes, Context, SpanContext } from '@opentelemetry/api'; +import type { Attributes, Context, Span } from '@opentelemetry/api'; import { isSpanContextValid, trace } from '@opentelemetry/api'; import { TraceState } from '@opentelemetry/core'; import type { Sampler, SamplingResult } from '@opentelemetry/sdk-trace-base'; import { SamplingDecision } from '@opentelemetry/sdk-trace-base'; -import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, hasTracingEnabled } from '@sentry/core'; -import type { Client, ClientOptions, SamplingContext } from '@sentry/types'; -import { isNaN, logger } from '@sentry/utils'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, hasTracingEnabled, sampleSpan } from '@sentry/core'; +import type { Client, SpanAttributes } from '@sentry/types'; +import { logger } from '@sentry/utils'; import { SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING } from './constants'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; import { DEBUG_BUILD } from './debug-build'; -import { getPropagationContextFromSpanContext, getSamplingDecision } from './propagator'; +import { getPropagationContextFromSpan } from './propagator'; +import { getSamplingDecision } from './utils/getSamplingDecision'; import { setIsSetup } from './utils/setupCheck'; /** - * A custom OTEL sampler that uses Sentry sampling rates to make it's decision + * A custom OTEL sampler that uses Sentry sampling rates to make its decision */ export class SentrySampler implements Sampler { private _client: Client; @@ -29,7 +31,7 @@ export class SentrySampler implements Sampler { traceId: string, spanName: string, _spanKind: unknown, - spanAttributes: unknown, + spanAttributes: SpanAttributes, _links: unknown, ): SamplingResult { const options = this._client.getOptions(); @@ -38,16 +40,17 @@ export class SentrySampler implements Sampler { return { decision: SamplingDecision.NOT_RECORD }; } - const parentContext = trace.getSpanContext(context); + const parentSpan = trace.getSpan(context); + const parentContext = parentSpan?.spanContext(); const traceState = parentContext?.traceState || new TraceState(); let parentSampled: boolean | undefined = undefined; // Only inherit sample rate if `traceId` is the same // Note for testing: `isSpanContextValid()` checks the format of the traceId/spanId, so we need to pass valid ones - if (parentContext && isSpanContextValid(parentContext) && parentContext.traceId === traceId) { + if (parentSpan && parentContext && isSpanContextValid(parentContext) && parentContext.traceId === traceId) { if (parentContext.isRemote) { - parentSampled = getParentRemoteSampled(parentContext); + parentSampled = getParentRemoteSampled(parentSpan); DEBUG_BUILD && logger.log(`[Tracing] Inheriting remote parent's sampled decision for ${spanName}: ${parentSampled}`); } else { @@ -56,7 +59,7 @@ export class SentrySampler implements Sampler { } } - const sampleRate = getSampleRate(options, { + const [sampled, sampleRate] = sampleSpan(options, { name: spanName, attributes: spanAttributes, transactionContext: { @@ -67,32 +70,12 @@ export class SentrySampler implements Sampler { }); const attributes: Attributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: Number(sampleRate), + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: sampleRate, }; - // Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The - // only valid values are booleans or numbers between 0 and 1.) - if (!isValidSampleRate(sampleRate)) { - DEBUG_BUILD && logger.warn('[Tracing] Discarding span because of invalid sample rate.'); - - return { - decision: SamplingDecision.NOT_RECORD, - attributes, - traceState: traceState.set(SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING, '1'), - }; - } - - // if the function returned 0 (or false), or if `tracesSampleRate` is 0, it's a sign the transaction should be dropped - if (!sampleRate) { - DEBUG_BUILD && - logger.log( - `[Tracing] Discarding span because ${ - typeof options.tracesSampler === 'function' - ? 'tracesSampler returned 0 or false' - : 'a negative sampling decision was inherited or tracesSampleRate is set to 0' - }`, - ); - + const method = `${spanAttributes[SemanticAttributes.HTTP_METHOD]}`.toUpperCase(); + if (method === 'OPTIONS' || method === 'HEAD') { + DEBUG_BUILD && logger.log(`[Tracing] Not sampling span because HTTP method is '${method}' for ${spanName}`); return { decision: SamplingDecision.NOT_RECORD, attributes, @@ -100,19 +83,7 @@ export class SentrySampler implements Sampler { }; } - // Now we roll the dice. Math.random is inclusive of 0, but not of 1, so strict < is safe here. In case sampleRate is - // a boolean, the < comparison will cause it to be automatically cast to 1 if it's true and 0 if it's false. - const isSampled = Math.random() < (sampleRate as number | boolean); - - // if we're not going to keep it, we're done - if (!isSampled) { - DEBUG_BUILD && - logger.log( - `[Tracing] Discarding span because it's not included in the random sample (sampling rate = ${Number( - sampleRate, - )})`, - ); - + if (!sampled) { return { decision: SamplingDecision.NOT_RECORD, attributes, @@ -132,57 +103,9 @@ export class SentrySampler implements Sampler { } } -function getSampleRate( - options: Pick, - samplingContext: SamplingContext, -): number | boolean { - if (typeof options.tracesSampler === 'function') { - return options.tracesSampler(samplingContext); - } - - if (samplingContext.parentSampled !== undefined) { - return samplingContext.parentSampled; - } - - if (typeof options.tracesSampleRate !== 'undefined') { - return options.tracesSampleRate; - } - - // When `enableTracing === true`, we use a sample rate of 100% - if (options.enableTracing) { - return 1; - } - - return 0; -} - -/** - * Checks the given sample rate to make sure it is valid type and value (a boolean, or a number between 0 and 1). - */ -function isValidSampleRate(rate: unknown): boolean { - // we need to check NaN explicitly because it's of type 'number' and therefore wouldn't get caught by this typecheck - if (isNaN(rate) || !(typeof rate === 'number' || typeof rate === 'boolean')) { - DEBUG_BUILD && - logger.warn( - `[Tracing] Given sample rate is invalid. Sample rate must be a boolean or a number between 0 and 1. Got ${JSON.stringify( - rate, - )} of type ${JSON.stringify(typeof rate)}.`, - ); - return false; - } - - // in case sampleRate is a boolean, it will get automatically cast to 1 if it's true and 0 if it's false - if (rate < 0 || rate > 1) { - DEBUG_BUILD && - logger.warn(`[Tracing] Given sample rate is invalid. Sample rate must be between 0 and 1. Got ${rate}.`); - return false; - } - return true; -} - -function getParentRemoteSampled(spanContext: SpanContext): boolean | undefined { - const traceId = spanContext.traceId; - const traceparentData = getPropagationContextFromSpanContext(spanContext); +function getParentRemoteSampled(parentSpan: Span): boolean | undefined { + const traceId = parentSpan.spanContext().traceId; + const traceparentData = getPropagationContextFromSpan(parentSpan); // Only inherit sampled if `traceId` is the same return traceparentData && traceId === traceparentData.traceId ? traceparentData.sampled : undefined; diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts index c7cfbd36f261..a2603fe91718 100644 --- a/packages/opentelemetry/src/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -2,7 +2,7 @@ import type { Span } from '@opentelemetry/api'; import { SpanKind } from '@opentelemetry/api'; import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; -import { captureEvent, getMetricSummaryJsonForSpan } from '@sentry/core'; +import { captureEvent, getMetricSummaryJsonForSpan, timedEventsToMeasurements } from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, @@ -138,7 +138,10 @@ function maybeSend(spans: ReadableSpan[]): ReadableSpan[] { transactionEvent.spans = spans; - // TODO Measurements are not yet implemented in OTEL + const measurements = timedEventsToMeasurements(span.events); + if (Object.keys(measurements).length) { + transactionEvent.measurements = measurements; + } captureEvent(transactionEvent); }); diff --git a/packages/opentelemetry/src/spanProcessor.ts b/packages/opentelemetry/src/spanProcessor.ts index a873faaa52d1..740afc2cc6fa 100644 --- a/packages/opentelemetry/src/spanProcessor.ts +++ b/packages/opentelemetry/src/spanProcessor.ts @@ -1,10 +1,14 @@ import type { Context } from '@opentelemetry/api'; -import { ROOT_CONTEXT, TraceFlags, trace } from '@opentelemetry/api'; +import { ROOT_CONTEXT, trace } from '@opentelemetry/api'; import type { ReadableSpan, Span, SpanProcessor as SpanProcessorInterface } from '@opentelemetry/sdk-trace-base'; -import { addChildSpanToSpan, getClient, getDefaultCurrentScope, getDefaultIsolationScope } from '@sentry/core'; -import { logger } from '@sentry/utils'; - -import { DEBUG_BUILD } from './debug-build'; +import { + addChildSpanToSpan, + getClient, + getDefaultCurrentScope, + getDefaultIsolationScope, + logSpanEnd, + logSpanStart, +} from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_PARENT_IS_REMOTE } from './semanticAttributes'; import { SentrySpanExporter } from './spanExporter'; import { getScopesFromContext } from './utils/contextData'; @@ -41,13 +45,17 @@ function onSpanStart(span: Span, parentContext: Context): void { setSpanScopes(span, scopes); } + logSpanStart(span); + const client = getClient(); client?.emit('spanStart', span); } -function onSpanEnd(span: ReadableSpan): void { +function onSpanEnd(span: Span): void { + logSpanEnd(span); + const client = getClient(); - client?.emit('spanEnd', span as Span); + client?.emit('spanEnd', span); } /** @@ -81,22 +89,10 @@ export class SentrySpanProcessor implements SpanProcessorInterface { */ public onStart(span: Span, parentContext: Context): void { onSpanStart(span, parentContext); - - // TODO (v8): Trigger client `spanStart` & `spanEnd` in here, - // once we decoupled opentelemetry from SentrySpan - - DEBUG_BUILD && logger.log(`[Tracing] Starting span "${span.name}" (${span.spanContext().spanId})`); } /** @inheritDoc */ - public onEnd(span: ReadableSpan): void { - if (span.spanContext().traceFlags !== TraceFlags.SAMPLED) { - DEBUG_BUILD && logger.log(`[Tracing] Finishing unsampled span "${span.name}" (${span.spanContext().spanId})`); - return; - } - - DEBUG_BUILD && logger.log(`[Tracing] Finishing span "${span.name}" (${span.spanContext().spanId})`); - + public onEnd(span: Span & ReadableSpan): void { onSpanEnd(span); this._exporter.export(span); diff --git a/packages/opentelemetry/src/trace.ts b/packages/opentelemetry/src/trace.ts index 31bdb46eec67..91fd5832b1fe 100644 --- a/packages/opentelemetry/src/trace.ts +++ b/packages/opentelemetry/src/trace.ts @@ -9,17 +9,17 @@ import { continueTrace as baseContinueTrace, getClient, getCurrentScope, - getDynamicSamplingContextFromClient, getRootSpan, handleCallbackErrors, spanToJSON, } from '@sentry/core'; import type { Client, Scope } from '@sentry/types'; -import { continueTraceAsRemoteSpan, getSamplingDecision, makeTraceState } from './propagator'; +import { continueTraceAsRemoteSpan, makeTraceState } from './propagator'; import type { OpenTelemetryClient, OpenTelemetrySpanContext } from './types'; import { getContextFromScope, getScopesFromContext } from './utils/contextData'; import { getDynamicSamplingContextFromSpan } from './utils/dynamicSamplingContext'; +import { getSamplingDecision } from './utils/getSamplingDecision'; /** * Wraps a function with a transaction/span and finishes the span after the function is done. @@ -97,11 +97,6 @@ export function startSpanManual( }); } -/** - * @deprecated Use {@link startSpan} instead. - */ -export const startActiveSpan = startSpan; - /** * Creates a span. This span is not set as active, so will not get automatic instrumentation spans * as children or be able to be accessed via `Sentry.getActiveSpan()`. @@ -186,13 +181,12 @@ function getContext(scope: Scope | undefined, forceTransaction: boolean | undefi if (actualScope && client) { const propagationContext = actualScope.getPropagationContext(); - const dynamicSamplingContext = - propagationContext.dsc || getDynamicSamplingContextFromClient(propagationContext.traceId, client); // We store the DSC as OTEL trace state on the span context const traceState = makeTraceState({ parentSpanId: propagationContext.parentSpanId, - dsc: dynamicSamplingContext, + // Not defined yet, we want to pick this up on-demand only + dsc: undefined, sampled: propagationContext.sampled, }); @@ -227,6 +221,8 @@ function getContext(scope: Scope | undefined, forceTransaction: boolean | undefi const { spanId, traceId } = parentSpan.spanContext(); const sampled = getSamplingDecision(parentSpan.spanContext()); + // In this case, when we are forcing a transaction, we want to treat this like continuing an incoming trace + // so we set the traceState according to the root span const rootSpan = getRootSpan(parentSpan); const dsc = getDynamicSamplingContextFromSpan(rootSpan); diff --git a/packages/opentelemetry/src/utils/dynamicSamplingContext.ts b/packages/opentelemetry/src/utils/dynamicSamplingContext.ts index 8fcedf65c6a4..3e4f9f67ae84 100644 --- a/packages/opentelemetry/src/utils/dynamicSamplingContext.ts +++ b/packages/opentelemetry/src/utils/dynamicSamplingContext.ts @@ -7,8 +7,9 @@ import { import type { DynamicSamplingContext } from '@sentry/types'; import { baggageHeaderToDynamicSamplingContext } from '@sentry/utils'; import { SENTRY_TRACE_STATE_DSC } from '../constants'; -import { getSamplingDecision } from '../propagator'; import type { AbstractSpan } from '../types'; +import { getSamplingDecision } from './getSamplingDecision'; +import { parseSpanDescription } from './parseSpanDescription'; import { spanHasAttributes, spanHasName } from './spanTypes'; /** @@ -45,10 +46,12 @@ export function getDynamicSamplingContextFromSpan(span: AbstractSpan): Readonly< // We don't want to have a transaction name in the DSC if the source is "url" because URLs might contain PII const source = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; - const name = spanHasName(span) ? span.name : ''; - if (source !== 'url' && name) { - dsc.transaction = name; + // If the span has no name, we assume it is non-recording and want to opt out of using any description + const { description } = spanHasName(span) ? parseSpanDescription(span) : { description: '' }; + + if (source !== 'url' && description) { + dsc.transaction = description; } const sampled = getSamplingDecision(span.spanContext()); diff --git a/packages/opentelemetry/src/utils/getSamplingDecision.ts b/packages/opentelemetry/src/utils/getSamplingDecision.ts new file mode 100644 index 000000000000..05e2aba26525 --- /dev/null +++ b/packages/opentelemetry/src/utils/getSamplingDecision.ts @@ -0,0 +1,42 @@ +import type { SpanContext } from '@opentelemetry/api'; +import { TraceFlags } from '@opentelemetry/api'; +import { baggageHeaderToDynamicSamplingContext } from '@sentry/utils'; +import { SENTRY_TRACE_STATE_DSC, SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING } from '../constants'; + +/** + * OpenTelemetry only knows about SAMPLED or NONE decision, + * but for us it is important to differentiate between unset and unsampled. + * + * Both of these are identified as `traceFlags === TracegFlags.NONE`, + * but we additionally look at a special trace state to differentiate between them. + */ +export function getSamplingDecision(spanContext: SpanContext): boolean | undefined { + const { traceFlags, traceState } = spanContext; + + const sampledNotRecording = traceState ? traceState.get(SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING) === '1' : false; + + // If trace flag is `SAMPLED`, we interpret this as sampled + // If it is `NONE`, it could mean either it was sampled to be not recorder, or that it was not sampled at all + // For us this is an important difference, sow e look at the SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING + // to identify which it is + if (traceFlags === TraceFlags.SAMPLED) { + return true; + } + + if (sampledNotRecording) { + return false; + } + + // Fall back to DSC as a last resort, that may also contain `sampled`... + const dscString = traceState ? traceState.get(SENTRY_TRACE_STATE_DSC) : undefined; + const dsc = dscString ? baggageHeaderToDynamicSamplingContext(dscString) : undefined; + + if (dsc?.sampled === 'true') { + return true; + } + if (dsc?.sampled === 'false') { + return false; + } + + return undefined; +} diff --git a/packages/opentelemetry/test/propagator.test.ts b/packages/opentelemetry/test/propagator.test.ts index 66d846085cfe..244b0aa93d2b 100644 --- a/packages/opentelemetry/test/propagator.test.ts +++ b/packages/opentelemetry/test/propagator.test.ts @@ -11,8 +11,9 @@ import { suppressTracing } from '@opentelemetry/core'; import { addTracingExtensions, withScope } from '@sentry/core'; import { SENTRY_BAGGAGE_HEADER, SENTRY_SCOPES_CONTEXT_KEY, SENTRY_TRACE_HEADER } from '../src/constants'; -import { SentryPropagator, getSamplingDecision, makeTraceState } from '../src/propagator'; +import { SentryPropagator, makeTraceState } from '../src/propagator'; import { getScopesFromContext } from '../src/utils/contextData'; +import { getSamplingDecision } from '../src/utils/getSamplingDecision'; import { cleanupOtel, mockSdkInit } from './helpers/mockSdkInit'; beforeAll(() => { @@ -56,6 +57,7 @@ describe('SentryPropagator', () => { 'sentry-environment=production', 'sentry-release=1.0.0', 'sentry-public_key=abc', + 'sentry-sampled=true', 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', ], 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1', @@ -91,6 +93,7 @@ describe('SentryPropagator', () => { 'sentry-environment=production', 'sentry-release=1.0.0', 'sentry-public_key=abc', + 'sentry-sampled=false', 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', ], 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-0', @@ -285,7 +288,10 @@ describe('SentryPropagator', () => { 'sentry-environment=production', 'sentry-release=1.0.0', 'sentry-public_key=abc', + 'sentry-sample_rate=1', + 'sentry-sampled=true', 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', + 'sentry-transaction=test', ], 'd4cda95b652f4a1592b449d5929fda1b-{{spanId}}-1', true, @@ -336,7 +342,10 @@ describe('SentryPropagator', () => { 'sentry-environment=production', 'sentry-release=1.0.0', 'sentry-public_key=abc', + 'sentry-sample_rate=1', + 'sentry-sampled=true', 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', + 'sentry-transaction=test', ], 'd4cda95b652f4a1592b449d5929fda1b-{{spanId}}-1', undefined, @@ -357,6 +366,7 @@ describe('SentryPropagator', () => { 'sentry-release=1.0.0', 'sentry-public_key=abc', 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', + 'sentry-sampled=false', ], 'd4cda95b652f4a1592b449d5929fda1b-{{spanId}}-0', false, @@ -439,6 +449,7 @@ describe('SentryPropagator', () => { 'sentry-environment=production', 'sentry-release=1.0.0', 'sentry-public_key=abc', + 'sentry-sampled=true', 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', ], 'd4cda95b652f4a1592b449d5929fda1b-{{spanId}}-1', @@ -482,7 +493,10 @@ describe('SentryPropagator', () => { 'sentry-environment=production', 'sentry-release=1.0.0', 'sentry-public_key=abc', + 'sentry-sample_rate=1', + 'sentry-sampled=true', 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', + 'sentry-transaction=test', ].sort(), ); expect(carrier[SENTRY_TRACE_HEADER]).toBe( @@ -493,6 +507,7 @@ describe('SentryPropagator', () => { }, ); + const carrier2: Record = {}; context.with( trace.setSpanContext(ROOT_CONTEXT, { traceId: 'd4cda95b652f4a1592b449d5929fda1b', @@ -509,9 +524,9 @@ describe('SentryPropagator', () => { sampled: true, }); - propagator.inject(context.active(), carrier, defaultTextMapSetter); + propagator.inject(context.active(), carrier2, defaultTextMapSetter); - expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual( + expect(baggageToArray(carrier2[SENTRY_BAGGAGE_HEADER])).toEqual( [ 'sentry-environment=production', 'sentry-release=1.0.0', @@ -519,7 +534,7 @@ describe('SentryPropagator', () => { 'sentry-trace_id=TRACE_ID', ].sort(), ); - expect(carrier[SENTRY_TRACE_HEADER]).toBe('TRACE_ID-SPAN_ID-1'); + expect(carrier2[SENTRY_TRACE_HEADER]).toBe('TRACE_ID-SPAN_ID-1'); }); }, ); @@ -542,6 +557,89 @@ describe('SentryPropagator', () => { 'sentry-public_key=abc', 'sentry-environment=production', 'sentry-release=1.0.0', + 'sentry-sampled=true', + ].sort(), + ); + }); + + it('should include existing baggage header', () => { + const spanContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + }; + + const carrier = { + other: 'header', + baggage: 'foo=bar,other=yes', + }; + const context = trace.setSpanContext(ROOT_CONTEXT, spanContext); + const baggage = propagation.createBaggage(); + propagator.inject(propagation.setBaggage(context, baggage), carrier, defaultTextMapSetter); + expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual( + [ + 'foo=bar', + 'other=yes', + 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', + 'sentry-public_key=abc', + 'sentry-environment=production', + 'sentry-release=1.0.0', + 'sentry-sampled=true', + ].sort(), + ); + }); + + it('should include existing baggage array header', () => { + const spanContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + }; + + const carrier = { + other: 'header', + baggage: ['foo=bar,other=yes', 'other2=no'], + }; + const context = trace.setSpanContext(ROOT_CONTEXT, spanContext); + const baggage = propagation.createBaggage(); + propagator.inject(propagation.setBaggage(context, baggage), carrier, defaultTextMapSetter); + expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual( + [ + 'foo=bar', + 'other=yes', + 'other2=no', + 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', + 'sentry-public_key=abc', + 'sentry-environment=production', + 'sentry-release=1.0.0', + 'sentry-sampled=true', + ].sort(), + ); + }); + + it('should overwrite existing sentry baggage header', () => { + const spanContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + }; + + const carrier = { + baggage: 'foo=bar,other=yes,sentry-release=9.9.9,sentry-other=yes', + }; + const context = trace.setSpanContext(ROOT_CONTEXT, spanContext); + const baggage = propagation.createBaggage(); + propagator.inject(propagation.setBaggage(context, baggage), carrier, defaultTextMapSetter); + expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual( + [ + 'foo=bar', + 'other=yes', + 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', + 'sentry-public_key=abc', + 'sentry-environment=production', + 'sentry-other=yes', + 'sentry-release=1.0.0', + 'sentry-sampled=true', ].sort(), ); }); diff --git a/packages/opentelemetry/test/trace.test.ts b/packages/opentelemetry/test/trace.test.ts index 27ae4a285b8a..a871fe1fbeba 100644 --- a/packages/opentelemetry/test/trace.test.ts +++ b/packages/opentelemetry/test/trace.test.ts @@ -18,12 +18,14 @@ import { withScope, } from '@sentry/core'; import type { Event, Scope } from '@sentry/types'; -import { getSamplingDecision, makeTraceState } from '../src/propagator'; +import { makeTraceState } from '../src/propagator'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; import { continueTrace, startInactiveSpan, startSpan, startSpanManual } from '../src/trace'; import type { AbstractSpan } from '../src/types'; import { getDynamicSamplingContextFromSpan } from '../src/utils/dynamicSamplingContext'; import { getActiveSpan } from '../src/utils/getActiveSpan'; +import { getSamplingDecision } from '../src/utils/getSamplingDecision'; import { getSpanKind } from '../src/utils/getSpanKind'; import { spanHasAttributes, spanHasName } from '../src/utils/spanTypes'; import { cleanupOtel, mockSdkInit } from './helpers/mockSdkInit'; @@ -947,9 +949,13 @@ describe('trace', () => { expect(span).toBeDefined(); expect(spanToJSON(span).trace_id).toEqual(propagationContext.traceId); expect(spanToJSON(span).parent_span_id).toEqual(propagationContext.spanId); - expect(getDynamicSamplingContextFromSpan(span)).toEqual( - getDynamicSamplingContextFromClient(propagationContext.traceId, getClient()!), - ); + + expect(getDynamicSamplingContextFromSpan(span)).toEqual({ + ...getDynamicSamplingContextFromClient(propagationContext.traceId, getClient()!), + sample_rate: '1', + sampled: 'true', + transaction: 'test span', + }); }); }); @@ -1353,6 +1359,76 @@ describe('trace (sampling)', () => { }); }); +describe('HTTP methods (sampling)', () => { + beforeEach(() => { + mockSdkInit({ enableTracing: true }); + }); + + afterEach(() => { + cleanupOtel(); + }); + + it('does sample when HTTP method is other than OPTIONS or HEAD', () => { + const spanGET = startSpanManual( + { name: 'test span', attributes: { [SemanticAttributes.HTTP_METHOD]: 'GET' } }, + span => { + return span; + }, + ); + expect(spanIsSampled(spanGET)).toBe(true); + expect(getSamplingDecision(spanGET.spanContext())).toBe(true); + + const spanPOST = startSpanManual( + { name: 'test span', attributes: { [SemanticAttributes.HTTP_METHOD]: 'POST' } }, + span => { + return span; + }, + ); + expect(spanIsSampled(spanPOST)).toBe(true); + expect(getSamplingDecision(spanPOST.spanContext())).toBe(true); + + const spanPUT = startSpanManual( + { name: 'test span', attributes: { [SemanticAttributes.HTTP_METHOD]: 'PUT' } }, + span => { + return span; + }, + ); + expect(spanIsSampled(spanPUT)).toBe(true); + expect(getSamplingDecision(spanPUT.spanContext())).toBe(true); + + const spanDELETE = startSpanManual( + { name: 'test span', attributes: { [SemanticAttributes.HTTP_METHOD]: 'DELETE' } }, + span => { + return span; + }, + ); + expect(spanIsSampled(spanDELETE)).toBe(true); + expect(getSamplingDecision(spanDELETE.spanContext())).toBe(true); + }); + + it('does not sample when HTTP method is OPTIONS', () => { + const span = startSpanManual( + { name: 'test span', attributes: { [SemanticAttributes.HTTP_METHOD]: 'OPTIONS' } }, + span => { + return span; + }, + ); + expect(spanIsSampled(span)).toBe(false); + expect(getSamplingDecision(span.spanContext())).toBe(false); + }); + + it('does not sample when HTTP method is HEAD', () => { + const span = startSpanManual( + { name: 'test span', attributes: { [SemanticAttributes.HTTP_METHOD]: 'HEAD' } }, + span => { + return span; + }, + ); + expect(spanIsSampled(span)).toBe(false); + expect(getSamplingDecision(span.spanContext())).toBe(false); + }); +}); + describe('continueTrace', () => { beforeEach(() => { mockSdkInit({ enableTracing: true }); diff --git a/packages/react/test/profiler.test.tsx b/packages/react/test/profiler.test.tsx index 9f8b23063e00..07f34d526915 100644 --- a/packages/react/test/profiler.test.tsx +++ b/packages/react/test/profiler.test.tsx @@ -1,5 +1,5 @@ import { SentrySpan } from '@sentry/core'; -import type { SpanContext } from '@sentry/types'; +import type { StartSpanOptions } from '@sentry/types'; import { render } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; // biome-ignore lint/nursery/noUnusedImports: Need React import for JSX @@ -8,7 +8,7 @@ import * as React from 'react'; import { REACT_MOUNT_OP, REACT_RENDER_OP, REACT_UPDATE_OP } from '../src/constants'; import { UNKNOWN_COMPONENT, useProfiler, withProfiler } from '../src/profiler'; -const mockStartInactiveSpan = jest.fn((spanArgs: SpanContext) => ({ ...spanArgs })); +const mockStartInactiveSpan = jest.fn((spanArgs: StartSpanOptions) => ({ ...spanArgs })); const mockFinish = jest.fn(); class MockSpan extends SentrySpan { @@ -22,7 +22,7 @@ let activeSpan: Record; jest.mock('@sentry/browser', () => ({ ...jest.requireActual('@sentry/browser'), getActiveSpan: () => activeSpan, - startInactiveSpan: (ctx: SpanContext) => { + startInactiveSpan: (ctx: StartSpanOptions) => { mockStartInactiveSpan(ctx); return new MockSpan(ctx); }, diff --git a/packages/remix/package.json b/packages/remix/package.json index a1684da9f0c4..fdffcb49972f 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -17,7 +17,8 @@ "esm", "types", "types-ts3.8", - "scripts" + "scripts", + "register.mjs" ], "main": "build/cjs/index.server.js", "module": "build/esm/index.server.js", @@ -32,7 +33,17 @@ }, "node": "./build/cjs/index.server.js", "types": "./build/types/index.types.d.ts" - } + }, + "./register": { + "import": { + "default": "./build/register.mjs" + } + }, +"./hook": { + "import": { + "default": "./build/hook.mjs" + } +} }, "typesVersions": { "<4.9": { @@ -46,7 +57,7 @@ }, "dependencies": { "@remix-run/router": "1.x", - "@sentry/cli": "^2.30.2", + "@sentry/cli": "^2.31.0", "@sentry/core": "8.0.0-alpha.7", "@sentry/node": "8.0.0-alpha.7", "@sentry/opentelemetry": "8.0.0-alpha.7", diff --git a/packages/remix/rollup.npm.config.mjs b/packages/remix/rollup.npm.config.mjs index c588c260c703..b705fba0c55f 100644 --- a/packages/remix/rollup.npm.config.mjs +++ b/packages/remix/rollup.npm.config.mjs @@ -1,14 +1,17 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; +import { makeBaseNPMConfig, makeNPMConfigVariants, makeOtelLoaders } from '@sentry-internal/rollup-utils'; -export default makeNPMConfigVariants( - makeBaseNPMConfig({ - entrypoints: ['src/index.server.ts', 'src/index.client.tsx'], - packageSpecificConfig: { - external: ['react-router', 'react-router-dom'], - output: { - // make it so Rollup calms down about the fact that we're combining default and named exports - exports: 'named', +export default [ + ...makeNPMConfigVariants( + makeBaseNPMConfig({ + entrypoints: ['src/index.server.ts', 'src/index.client.tsx'], + packageSpecificConfig: { + external: ['react-router', 'react-router-dom'], + output: { + // make it so Rollup calms down about the fact that we're combining default and named exports + exports: 'named', + }, }, - }, - }), -); + }), + ), + ...makeOtelLoaders('./build', 'sentry-node'), +]; diff --git a/packages/remix/src/client/performance.tsx b/packages/remix/src/client/performance.tsx index f50bd5ac2a0e..d92ad323beb1 100644 --- a/packages/remix/src/client/performance.tsx +++ b/packages/remix/src/client/performance.tsx @@ -2,6 +2,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, getActiveSpan, + getCurrentScope, getRootSpan, } from '@sentry/core'; import type { browserTracingIntegration as originalBrowserTracingIntegration } from '@sentry/react'; @@ -13,7 +14,7 @@ import { startBrowserTracingPageLoadSpan, withErrorBoundary, } from '@sentry/react'; -import type { Span, StartSpanOptions, TransactionContext } from '@sentry/types'; +import type { StartSpanOptions } from '@sentry/types'; import { isNodeEnv, logger } from '@sentry/utils'; import * as React from 'react'; @@ -52,7 +53,6 @@ let _useEffect: UseEffect | undefined; let _useLocation: UseLocation | undefined; let _useMatches: UseMatches | undefined; -let _customStartTransaction: ((context: TransactionContext) => Span | undefined) | undefined; let _instrumentNavigation: boolean | undefined; function getInitPathName(): string | undefined { @@ -83,18 +83,13 @@ export function startPageloadSpan(): void { }, }; - // If _customStartTransaction is not defined, we know that we are using the browserTracingIntegration - if (!_customStartTransaction) { - const client = getClient(); + const client = getClient(); - if (!client) { - return; - } - - startBrowserTracingPageLoadSpan(client, spanContext); - } else { - _customStartTransaction(spanContext); + if (!client) { + return; } + + startBrowserTracingPageLoadSpan(client, spanContext); } function startNavigationSpan(matches: RouteMatch[]): void { @@ -107,18 +102,13 @@ function startNavigationSpan(matches: RouteMatch[]): void { }, }; - // If _customStartTransaction is not defined, we know that we are using the browserTracingIntegration - if (!_customStartTransaction) { - const client = getClient(); - - if (!client) { - return; - } + const client = getClient(); - startBrowserTracingNavigationSpan(client, spanContext); - } else { - _customStartTransaction(spanContext); + if (!client) { + return; } + + startBrowserTracingNavigationSpan(client, spanContext); } /** @@ -158,14 +148,18 @@ export function withSentry

    , R extends React.Co const matches = _useMatches(); _useEffect(() => { - const activeRootSpan = getActiveSpan(); + if (matches && matches.length) { + const routeName = matches[matches.length - 1].id; + getCurrentScope().setTransactionName(routeName); - if (activeRootSpan && matches && matches.length) { - const transaction = getRootSpan(activeRootSpan); + const activeRootSpan = getActiveSpan(); + if (activeRootSpan) { + const transaction = getRootSpan(activeRootSpan); - if (transaction) { - transaction.updateName(matches[matches.length - 1].id); - transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + if (transaction) { + transaction.updateName(routeName); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + } } } @@ -214,17 +208,14 @@ export function setGlobals({ useLocation, useMatches, instrumentNavigation, - customStartTransaction, }: { useEffect?: UseEffect; useLocation?: UseLocation; useMatches?: UseMatches; instrumentNavigation?: boolean; - customStartTransaction?: (context: TransactionContext) => Span | undefined; }): void { _useEffect = useEffect; _useLocation = useLocation; _useMatches = useMatches; _instrumentNavigation = instrumentNavigation; - _customStartTransaction = customStartTransaction; } diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts index e31c0b29fbd8..ff3d0eb5c51b 100644 --- a/packages/remix/src/index.server.ts +++ b/packages/remix/src/index.server.ts @@ -88,12 +88,14 @@ export { mysqlIntegration, mysql2Integration, nestIntegration, + setupNestErrorHandler, postgresIntegration, prismaIntegration, hapiIntegration, setupHapiErrorHandler, spotlightIntegration, setupFastifyErrorHandler, + trpcMiddleware, } from '@sentry/node'; // Keeping the `*` exports for backwards compatibility and types diff --git a/packages/remix/src/index.types.ts b/packages/remix/src/index.types.ts index cbbf17998188..85bbb1a56235 100644 --- a/packages/remix/src/index.types.ts +++ b/packages/remix/src/index.types.ts @@ -12,10 +12,6 @@ import type { RemixOptions } from './utils/remixOptions'; /** Initializes Sentry Remix SDK */ export declare function init(options: RemixOptions): void; -// We export a merged Integrations object so that users can (at least typing-wise) use all integrations everywhere. -// eslint-disable-next-line deprecation/deprecation -export declare const Integrations: typeof clientSdk.Integrations; - export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; diff --git a/packages/remix/test/integration/package.json b/packages/remix/test/integration/package.json index e5a763551fe9..63560ec64e8b 100644 --- a/packages/remix/test/integration/package.json +++ b/packages/remix/test/integration/package.json @@ -25,17 +25,18 @@ "resolutions": { "@sentry/browser": "file:../../../browser", "@sentry/core": "file:../../../core", - "@sentry/node": "file:../../../node-experimental", + "@sentry/node": "file:../../../node", "@sentry/opentelemetry": "file:../../../opentelemetry", "@sentry/react": "file:../../../react", + "@sentry-internal/browser-utils": "file:../../../browser-utils", "@sentry-internal/replay": "file:../../../replay-internal", "@sentry-internal/replay-canvas": "file:../../../replay-canvas", - "@sentry-internal/tracing": "file:../../../tracing-internal", "@sentry-internal/feedback": "file:../../../feedback", "@sentry/types": "file:../../../types", "@sentry/utils": "file:../../../utils", "@vanilla-extract/css": "1.13.0", - "@vanilla-extract/integration": "6.2.4" + "@vanilla-extract/integration": "6.2.4", + "@types/mime": "^3.0.0" }, "engines": { "node": ">=14.18" diff --git a/packages/remix/test/integration/test/client/capture-exception.test.ts b/packages/remix/test/integration/test/client/capture-exception.test.ts index 18f2fd9af196..b7e38abf2f4c 100644 --- a/packages/remix/test/integration/test/client/capture-exception.test.ts +++ b/packages/remix/test/integration/test/client/capture-exception.test.ts @@ -8,8 +8,7 @@ test('should report a manually captured error.', async ({ page }) => { const [errorEnvelope, pageloadEnvelope] = envelopes; expect(errorEnvelope.level).toBe('error'); - // TODO: Comment back in once we update the scope transaction name on the client side - // expect(errorEnvelope.transaction).toBe('/capture-exception'); + expect(errorEnvelope.transaction).toBe('/capture-exception'); expect(errorEnvelope.exception?.values).toMatchObject([ { type: 'Error', diff --git a/packages/remix/test/integration/test/client/capture-message.test.ts b/packages/remix/test/integration/test/client/capture-message.test.ts index 5c3e578ad81f..234b6ee3d961 100644 --- a/packages/remix/test/integration/test/client/capture-message.test.ts +++ b/packages/remix/test/integration/test/client/capture-message.test.ts @@ -8,8 +8,7 @@ test('should report a manually captured message.', async ({ page }) => { const [messageEnvelope, pageloadEnvelope] = envelopes; expect(messageEnvelope.level).toBe('info'); - // TODO: Comment back in once we update the scope transaction name on the client side - // expect(messageEnvelope.transaction).toBe('/capture-message'); + expect(messageEnvelope.transaction).toBe('/capture-message'); expect(messageEnvelope.message).toBe('Sentry Manually Captured Message'); expect(pageloadEnvelope.contexts?.trace?.op).toBe('pageload'); diff --git a/packages/remix/test/integration/test/client/errorboundary.test.ts b/packages/remix/test/integration/test/client/errorboundary.test.ts index b1af46338345..6d249d563a41 100644 --- a/packages/remix/test/integration/test/client/errorboundary.test.ts +++ b/packages/remix/test/integration/test/client/errorboundary.test.ts @@ -39,4 +39,7 @@ test('should capture React component errors.', async ({ page }) => { mechanism: { type: useV2 ? 'instrument' : 'generic', handled: !useV2 }, }, ]); + expect(errorEnvelope.transaction).toBe( + useV2 ? 'routes/error-boundary-capture.$id' : 'routes/error-boundary-capture/$id', + ); }); diff --git a/packages/replay-canvas/package.json b/packages/replay-canvas/package.json index beca075b25a4..938d20195538 100644 --- a/packages/replay-canvas/package.json +++ b/packages/replay-canvas/package.json @@ -69,7 +69,7 @@ "homepage": "https://docs.sentry.io/platforms/javascript/session-replay/", "devDependencies": { "@babel/core": "^7.17.5", - "@sentry-internal/rrweb": "2.11.0" + "@sentry-internal/rrweb": "2.12.0" }, "dependencies": { "@sentry-internal/replay": "8.0.0-alpha.7", diff --git a/packages/replay-canvas/rollup.npm.config.mjs b/packages/replay-canvas/rollup.npm.config.mjs index 8c50a33f0afb..3b4431fa6829 100644 --- a/packages/replay-canvas/rollup.npm.config.mjs +++ b/packages/replay-canvas/rollup.npm.config.mjs @@ -9,7 +9,10 @@ export default makeNPMConfigVariants( exports: 'named', // set preserveModules to false because for Replay we actually want // to bundle everything into one file. - preserveModules: false, + preserveModules: + process.env.SENTRY_BUILD_PRESERVE_MODULES === undefined + ? false + : Boolean(process.env.SENTRY_BUILD_PRESERVE_MODULES), }, }, }), diff --git a/packages/replay-internal/package.json b/packages/replay-internal/package.json index 670a0b3c97b3..2c84ed0509e4 100644 --- a/packages/replay-internal/package.json +++ b/packages/replay-internal/package.json @@ -70,13 +70,13 @@ "devDependencies": { "@babel/core": "^7.17.5", "@sentry-internal/replay-worker": "8.0.0-alpha.7", - "@sentry-internal/rrweb": "2.11.0", - "@sentry-internal/rrweb-snapshot": "2.11.0", + "@sentry-internal/rrweb": "2.12.0", + "@sentry-internal/rrweb-snapshot": "2.12.0", "fflate": "^0.8.1", "jsdom-worker": "^0.2.1" }, "dependencies": { - "@sentry-internal/tracing": "8.0.0-alpha.7", + "@sentry-internal/browser-utils": "8.0.0-alpha.7", "@sentry/core": "8.0.0-alpha.7", "@sentry/types": "8.0.0-alpha.7", "@sentry/utils": "8.0.0-alpha.7" diff --git a/packages/replay-internal/rollup.npm.config.mjs b/packages/replay-internal/rollup.npm.config.mjs index 8c50a33f0afb..3b4431fa6829 100644 --- a/packages/replay-internal/rollup.npm.config.mjs +++ b/packages/replay-internal/rollup.npm.config.mjs @@ -9,7 +9,10 @@ export default makeNPMConfigVariants( exports: 'named', // set preserveModules to false because for Replay we actually want // to bundle everything into one file. - preserveModules: false, + preserveModules: + process.env.SENTRY_BUILD_PRESERVE_MODULES === undefined + ? false + : Boolean(process.env.SENTRY_BUILD_PRESERVE_MODULES), }, }, }), diff --git a/packages/replay-internal/src/coreHandlers/performanceObserver.ts b/packages/replay-internal/src/coreHandlers/performanceObserver.ts index 8039c659ca6c..45b843760e52 100644 --- a/packages/replay-internal/src/coreHandlers/performanceObserver.ts +++ b/packages/replay-internal/src/coreHandlers/performanceObserver.ts @@ -1,4 +1,4 @@ -import { addLcpInstrumentationHandler, addPerformanceInstrumentationHandler } from '@sentry-internal/tracing'; +import { addLcpInstrumentationHandler, addPerformanceInstrumentationHandler } from '@sentry-internal/browser-utils'; import type { ReplayContainer } from '../types'; import { getLargestContentfulPaint } from '../util/createPerformanceEntries'; diff --git a/packages/replay-internal/src/coreHandlers/util/getAttributesToRecord.ts b/packages/replay-internal/src/coreHandlers/util/getAttributesToRecord.ts index f50c2b9f9088..d2d062926811 100644 --- a/packages/replay-internal/src/coreHandlers/util/getAttributesToRecord.ts +++ b/packages/replay-internal/src/coreHandlers/util/getAttributesToRecord.ts @@ -20,6 +20,9 @@ const ATTRIBUTES_TO_RECORD = new Set([ */ export function getAttributesToRecord(attributes: Record): Record { const obj: Record = {}; + if (!attributes['data-sentry-component'] && attributes['data-sentry-element']) { + attributes['data-sentry-component'] = attributes['data-sentry-element']; + } for (const key in attributes) { if (ATTRIBUTES_TO_RECORD.has(key)) { let normalizedKey = key; diff --git a/packages/replay-internal/src/integration.ts b/packages/replay-internal/src/integration.ts index 89b1cd8709ad..f4759ebff26c 100644 --- a/packages/replay-internal/src/integration.ts +++ b/packages/replay-internal/src/integration.ts @@ -1,4 +1,4 @@ -import { getClient } from '@sentry/core'; +import { getClient, parseSampleRate } from '@sentry/core'; import type { BrowserClientReplayOptions, Integration, IntegrationFn } from '@sentry/types'; import { consoleSandbox, dropUndefinedKeys, isBrowser } from '@sentry/utils'; @@ -367,7 +367,10 @@ function loadReplayOptionsFromClient(initialOptions: InitialReplayPluginOptions) return finalOptions; } - if (opt.replaysSessionSampleRate == null && opt.replaysOnErrorSampleRate == null) { + const replaysSessionSampleRate = parseSampleRate(opt.replaysSessionSampleRate); + const replaysOnErrorSampleRate = parseSampleRate(opt.replaysOnErrorSampleRate); + + if (replaysSessionSampleRate == null && replaysOnErrorSampleRate == null) { consoleSandbox(() => { // eslint-disable-next-line no-console console.warn( @@ -376,12 +379,12 @@ function loadReplayOptionsFromClient(initialOptions: InitialReplayPluginOptions) }); } - if (typeof opt.replaysSessionSampleRate === 'number') { - finalOptions.sessionSampleRate = opt.replaysSessionSampleRate; + if (replaysSessionSampleRate != null) { + finalOptions.sessionSampleRate = replaysSessionSampleRate; } - if (typeof opt.replaysOnErrorSampleRate === 'number') { - finalOptions.errorSampleRate = opt.replaysOnErrorSampleRate; + if (replaysOnErrorSampleRate != null) { + finalOptions.errorSampleRate = replaysOnErrorSampleRate; } return finalOptions; diff --git a/packages/replay-internal/test/integration/integrationSettings.test.ts b/packages/replay-internal/test/integration/integrationSettings.test.ts index 118b0511ffce..3d7c180faf2f 100644 --- a/packages/replay-internal/test/integration/integrationSettings.test.ts +++ b/packages/replay-internal/test/integration/integrationSettings.test.ts @@ -37,6 +37,19 @@ describe('Integration | integrationSettings', () => { expect(replay.getOptions().sessionSampleRate).toBe(0); expect(mockConsole).toBeCalledTimes(0); }); + + it('works with defining a string rate in SDK', async () => { + const { replay } = await mockSdk({ + sentryOptions: { + // @ts-expect-error We want to test setting a string here + replaysSessionSampleRate: '0.5', + }, + replayOptions: {}, + }); + + expect(replay.getOptions().sessionSampleRate).toStrictEqual(0.5); + expect(mockConsole).toBeCalledTimes(0); + }); }); describe('replaysOnErrorSampleRate', () => { @@ -63,6 +76,19 @@ describe('Integration | integrationSettings', () => { expect(replay.getOptions().errorSampleRate).toBe(0); expect(mockConsole).toBeCalledTimes(0); }); + + it('works with defining a string rate in SDK', async () => { + const { replay } = await mockSdk({ + sentryOptions: { + // @ts-expect-error We want to test setting a string here + replaysOnErrorSampleRate: '0.5', + }, + replayOptions: {}, + }); + + expect(replay.getOptions().errorSampleRate).toStrictEqual(0.5); + expect(mockConsole).toBeCalledTimes(0); + }); }); describe('all sample rates', () => { diff --git a/packages/replay-internal/test/unit/coreHandlers/util/getAttributesToRecord.test.ts b/packages/replay-internal/test/unit/coreHandlers/util/getAttributesToRecord.test.ts index 46211820e2c7..393d58427e34 100644 --- a/packages/replay-internal/test/unit/coreHandlers/util/getAttributesToRecord.test.ts +++ b/packages/replay-internal/test/unit/coreHandlers/util/getAttributesToRecord.test.ts @@ -30,3 +30,21 @@ it('records only included attributes', function () { }), ).toEqual({}); }); + +it('records data-sentry-element as data-sentry-component when appropriate', function () { + expect( + getAttributesToRecord({ + ['data-sentry-component']: 'component', + ['data-sentry-element']: 'element', + }), + ).toEqual({ + ['data-sentry-component']: 'component', + }); + expect( + getAttributesToRecord({ + ['data-sentry-element']: 'element', + }), + ).toEqual({ + ['data-sentry-component']: 'element', + }); +}); diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index 2a4c6efa766c..e236e663c52f 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -37,7 +37,6 @@ "@sveltejs/kit": "1.x || 2.x" }, "dependencies": { - "@sentry-internal/tracing": "8.0.0-alpha.7", "@sentry/core": "8.0.0-alpha.7", "@sentry/node": "8.0.0-alpha.7", "@sentry/opentelemetry": "8.0.0-alpha.7", @@ -70,7 +69,7 @@ "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", "test": "yarn test:unit", - "test:unit": "vitest run --outputDiffMaxLines=2000", + "test:unit": "vitest run", "test:watch": "vitest --watch", "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push --sig" }, diff --git a/packages/sveltekit/src/client/browserTracingIntegration.ts b/packages/sveltekit/src/client/browserTracingIntegration.ts index 716223a17c5c..d2a266e2cbea 100644 --- a/packages/sveltekit/src/client/browserTracingIntegration.ts +++ b/packages/sveltekit/src/client/browserTracingIntegration.ts @@ -3,6 +3,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } fr import { WINDOW, browserTracingIntegration as originalBrowserTracingIntegration, + getCurrentScope, startBrowserTracingNavigationSpan, startBrowserTracingPageLoadSpan, startInactiveSpan, @@ -65,6 +66,7 @@ function _instrumentPageload(client: Client): void { if (routeId) { pageloadSpan.updateName(routeId); pageloadSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + getCurrentScope().setTransactionName(routeId); } }); } diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 0bb2f86e82f3..066ede9295aa 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -69,6 +69,7 @@ export { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + trpcMiddleware, } from '@sentry/node'; // We can still leave this for the carrier init and type exports diff --git a/packages/sveltekit/test/client/browserTracingIntegration.test.ts b/packages/sveltekit/test/client/browserTracingIntegration.test.ts index 1075d21f3b86..cdbe093e201b 100644 --- a/packages/sveltekit/test/client/browserTracingIntegration.test.ts +++ b/packages/sveltekit/test/client/browserTracingIntegration.test.ts @@ -40,10 +40,6 @@ describe('browserTracingIntegration', () => { ...txnCtx, updateName: vi.fn(), setAttribute: vi.fn(), - startChild: vi.fn().mockImplementation(ctx => { - return { ...mockedRoutingSpan, ...ctx }; - }), - setTag: vi.fn(), }; return createdRootSpan; }); @@ -55,7 +51,6 @@ describe('browserTracingIntegration', () => { ...txnCtx, updateName: vi.fn(), setAttribute: vi.fn(), - setTag: vi.fn(), }; return createdRootSpan; }); @@ -141,6 +136,29 @@ describe('browserTracingIntegration', () => { expect(startBrowserTracingPageLoadSpanSpy).toHaveBeenCalledTimes(0); }); + it("updates the current scope's transactionName once it's resolved during pageload", () => { + const scopeSetTransactionNameSpy = vi.fn(); + + // @ts-expect-error - only returning a partial scope here, that's fine + vi.spyOn(SentrySvelte, 'getCurrentScope').mockImplementation(() => { + return { + setTransactionName: scopeSetTransactionNameSpy, + }; + }); + + const integration = browserTracingIntegration(); + // @ts-expect-error - the fakeClient doesn't satisfy Client but that's fine + integration.afterAllSetup(fakeClient); + + // We emit an update to the `page` store to simulate the SvelteKit router lifecycle + // @ts-expect-error - page is a writable but the types say it's just readable + page.set({ route: { id: 'testRoute/:id' } }); + + // This should update the transaction name with the parameterized route: + expect(scopeSetTransactionNameSpy).toHaveBeenCalledTimes(3); + expect(scopeSetTransactionNameSpy).toHaveBeenLastCalledWith('testRoute/:id'); + }); + it("doesn't start a navigation span when `instrumentNavigation` is false", () => { const integration = browserTracingIntegration({ instrumentNavigation: false, @@ -188,7 +206,6 @@ describe('browserTracingIntegration', () => { }, }); - // eslint-disable-next-line deprecation/deprecation expect(startInactiveSpanSpy).toHaveBeenCalledWith({ op: 'ui.sveltekit.routing', name: 'SvelteKit Route Change', @@ -251,7 +268,6 @@ describe('browserTracingIntegration', () => { }, }); - // eslint-disable-next-line deprecation/deprecation expect(startInactiveSpanSpy).toHaveBeenCalledWith({ op: 'ui.sveltekit.routing', name: 'SvelteKit Route Change', diff --git a/packages/tracing-internal/README.md b/packages/tracing-internal/README.md deleted file mode 100644 index 76f035499f0d..000000000000 --- a/packages/tracing-internal/README.md +++ /dev/null @@ -1,12 +0,0 @@ -

    - - Sentry - -

    - -## Sentry Internal Tracing Package - Do not use directly, for internal use only - -This is an internal package that is being used to migrate @sentry/tracing code to its respective runtime packages. - -For v8, @sentry/tracing will be dropped and the code in this package will be split into @sentry/browser and -@sentry/node. diff --git a/packages/tracing-internal/jest.config.js b/packages/tracing-internal/jest.config.js deleted file mode 100644 index 24f49ab59a4c..000000000000 --- a/packages/tracing-internal/jest.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../jest/jest.config.js'); diff --git a/packages/tracing-internal/src/browser/web-vitals/getCLS.ts b/packages/tracing-internal/src/browser/web-vitals/getCLS.ts deleted file mode 100644 index fdd1e867adfa..000000000000 --- a/packages/tracing-internal/src/browser/web-vitals/getCLS.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { bindReporter } from './lib/bindReporter'; -import { initMetric } from './lib/initMetric'; -import { observe } from './lib/observe'; -import { onHidden } from './lib/onHidden'; -import type { CLSMetric, ReportCallback, StopListening } from './types'; - -/** - * Calculates the [CLS](https://web.dev/cls/) value for the current page and - * calls the `callback` function once the value is ready to be reported, along - * with all `layout-shift` performance entries that were used in the metric - * value calculation. The reported value is a `double` (corresponding to a - * [layout shift score](https://web.dev/cls/#layout-shift-score)). - * - * If the `reportAllChanges` configuration option is set to `true`, the - * `callback` function will be called as soon as the value is initially - * determined as well as any time the value changes throughout the page - * lifespan. - * - * _**Important:** CLS should be continually monitored for changes throughout - * the entire lifespan of a page—including if the user returns to the page after - * it's been hidden/backgrounded. However, since browsers often [will not fire - * additional callbacks once the user has backgrounded a - * page](https://developer.chrome.com/blog/page-lifecycle-api/#advice-hidden), - * `callback` is always called when the page's visibility state changes to - * hidden. As a result, the `callback` function might be called multiple times - * during the same page load._ - */ -export const onCLS = (onReport: ReportCallback): StopListening | undefined => { - const metric = initMetric('CLS', 0); - let report: ReturnType; - - let sessionValue = 0; - let sessionEntries: PerformanceEntry[] = []; - - // const handleEntries = (entries: Metric['entries']) => { - const handleEntries = (entries: LayoutShift[]): void => { - entries.forEach(entry => { - // Only count layout shifts without recent user input. - if (!entry.hadRecentInput) { - const firstSessionEntry = sessionEntries[0]; - const lastSessionEntry = sessionEntries[sessionEntries.length - 1]; - - // If the entry occurred less than 1 second after the previous entry and - // less than 5 seconds after the first entry in the session, include the - // entry in the current session. Otherwise, start a new session. - if ( - sessionValue && - sessionEntries.length !== 0 && - entry.startTime - lastSessionEntry.startTime < 1000 && - entry.startTime - firstSessionEntry.startTime < 5000 - ) { - sessionValue += entry.value; - sessionEntries.push(entry); - } else { - sessionValue = entry.value; - sessionEntries = [entry]; - } - - // If the current session value is larger than the current CLS value, - // update CLS and the entries contributing to it. - if (sessionValue > metric.value) { - metric.value = sessionValue; - metric.entries = sessionEntries; - if (report) { - report(); - } - } - } - }); - }; - - const po = observe('layout-shift', handleEntries); - if (po) { - report = bindReporter(onReport, metric); - - const stopListening = (): void => { - handleEntries(po.takeRecords() as CLSMetric['entries']); - report(true); - }; - - onHidden(stopListening); - - return stopListening; - } - - return; -}; diff --git a/packages/tracing-internal/src/browser/web-vitals/getFID.ts b/packages/tracing-internal/src/browser/web-vitals/getFID.ts deleted file mode 100644 index fd19e112121a..000000000000 --- a/packages/tracing-internal/src/browser/web-vitals/getFID.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { bindReporter } from './lib/bindReporter'; -import { getVisibilityWatcher } from './lib/getVisibilityWatcher'; -import { initMetric } from './lib/initMetric'; -import { observe } from './lib/observe'; -import { onHidden } from './lib/onHidden'; -import type { FIDMetric, PerformanceEventTiming, ReportCallback } from './types'; - -/** - * Calculates the [FID](https://web.dev/fid/) value for the current page and - * calls the `callback` function once the value is ready, along with the - * relevant `first-input` performance entry used to determine the value. The - * reported value is a `DOMHighResTimeStamp`. - * - * _**Important:** since FID is only reported after the user interacts with the - * page, it's possible that it will not be reported for some page loads._ - */ -export const onFID = (onReport: ReportCallback): void => { - const visibilityWatcher = getVisibilityWatcher(); - const metric = initMetric('FID'); - // eslint-disable-next-line prefer-const - let report: ReturnType; - - const handleEntry = (entry: PerformanceEventTiming): void => { - // Only report if the page wasn't hidden prior to the first input. - if (entry.startTime < visibilityWatcher.firstHiddenTime) { - metric.value = entry.processingStart - entry.startTime; - metric.entries.push(entry); - report(true); - } - }; - - const handleEntries = (entries: FIDMetric['entries']): void => { - (entries as PerformanceEventTiming[]).forEach(handleEntry); - }; - - const po = observe('first-input', handleEntries); - report = bindReporter(onReport, metric); - - if (po) { - onHidden(() => { - handleEntries(po.takeRecords() as FIDMetric['entries']); - po.disconnect(); - }, true); - } -}; diff --git a/packages/tracing-internal/src/browser/web-vitals/getLCP.ts b/packages/tracing-internal/src/browser/web-vitals/getLCP.ts deleted file mode 100644 index 37e37c01eebd..000000000000 --- a/packages/tracing-internal/src/browser/web-vitals/getLCP.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { bindReporter } from './lib/bindReporter'; -import { getActivationStart } from './lib/getActivationStart'; -import { getVisibilityWatcher } from './lib/getVisibilityWatcher'; -import { initMetric } from './lib/initMetric'; -import { observe } from './lib/observe'; -import { onHidden } from './lib/onHidden'; -import type { LCPMetric, ReportCallback, StopListening } from './types'; - -const reportedMetricIDs: Record = {}; - -/** - * Calculates the [LCP](https://web.dev/lcp/) value for the current page and - * calls the `callback` function once the value is ready (along with the - * relevant `largest-contentful-paint` performance entry used to determine the - * value). The reported value is a `DOMHighResTimeStamp`. - */ -export const onLCP = (onReport: ReportCallback): StopListening | undefined => { - const visibilityWatcher = getVisibilityWatcher(); - const metric = initMetric('LCP'); - let report: ReturnType; - - const handleEntries = (entries: LCPMetric['entries']): void => { - const lastEntry = entries[entries.length - 1] as LargestContentfulPaint; - if (lastEntry) { - // The startTime attribute returns the value of the renderTime if it is - // not 0, and the value of the loadTime otherwise. The activationStart - // reference is used because LCP should be relative to page activation - // rather than navigation start if the page was prerendered. - const value = Math.max(lastEntry.startTime - getActivationStart(), 0); - - // Only report if the page wasn't hidden prior to LCP. - if (value < visibilityWatcher.firstHiddenTime) { - metric.value = value; - metric.entries = [lastEntry]; - report(); - } - } - }; - - const po = observe('largest-contentful-paint', handleEntries); - - if (po) { - report = bindReporter(onReport, metric); - - const stopListening = (): void => { - if (!reportedMetricIDs[metric.id]) { - handleEntries(po.takeRecords() as LCPMetric['entries']); - po.disconnect(); - reportedMetricIDs[metric.id] = true; - report(true); - } - }; - - // Stop listening after input. Note: while scrolling is an input that - // stop LCP observation, it's unreliable since it can be programmatically - // generated. See: https://github.com/GoogleChrome/web-vitals/issues/75 - ['keydown', 'click'].forEach(type => { - addEventListener(type, stopListening, { once: true, capture: true }); - }); - - onHidden(stopListening, true); - - return stopListening; - } - - return; -}; diff --git a/packages/tracing-internal/src/browser/web-vitals/lib/getNavigationEntry.ts b/packages/tracing-internal/src/browser/web-vitals/lib/getNavigationEntry.ts deleted file mode 100644 index 75ec564eb5de..000000000000 --- a/packages/tracing-internal/src/browser/web-vitals/lib/getNavigationEntry.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { WINDOW } from '../../types'; -import type { NavigationTimingPolyfillEntry } from '../types'; - -const getNavigationEntryFromPerformanceTiming = (): NavigationTimingPolyfillEntry => { - // eslint-disable-next-line deprecation/deprecation - const timing = WINDOW.performance.timing; - // eslint-disable-next-line deprecation/deprecation - const type = WINDOW.performance.navigation.type; - - const navigationEntry: { [key: string]: number | string } = { - entryType: 'navigation', - startTime: 0, - type: type == 2 ? 'back_forward' : type === 1 ? 'reload' : 'navigate', - }; - - for (const key in timing) { - if (key !== 'navigationStart' && key !== 'toJSON') { - // eslint-disable-next-line deprecation/deprecation - navigationEntry[key] = Math.max((timing[key as keyof PerformanceTiming] as number) - timing.navigationStart, 0); - } - } - return navigationEntry as unknown as NavigationTimingPolyfillEntry; -}; - -export const getNavigationEntry = (): PerformanceNavigationTiming | NavigationTimingPolyfillEntry | undefined => { - if (WINDOW.__WEB_VITALS_POLYFILL__) { - return ( - WINDOW.performance && - ((performance.getEntriesByType && performance.getEntriesByType('navigation')[0]) || - getNavigationEntryFromPerformanceTiming()) - ); - } else { - return WINDOW.performance && performance.getEntriesByType && performance.getEntriesByType('navigation')[0]; - } -}; diff --git a/packages/tracing-internal/src/browser/web-vitals/lib/getVisibilityWatcher.ts b/packages/tracing-internal/src/browser/web-vitals/lib/getVisibilityWatcher.ts deleted file mode 100644 index f47ab0d82cf3..000000000000 --- a/packages/tracing-internal/src/browser/web-vitals/lib/getVisibilityWatcher.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { WINDOW } from '../../types'; -import { onHidden } from './onHidden'; - -let firstHiddenTime = -1; - -const initHiddenTime = (): number => { - // If the document is hidden and not prerendering, assume it was always - // hidden and the page was loaded in the background. - return WINDOW.document.visibilityState === 'hidden' && !WINDOW.document.prerendering ? 0 : Infinity; -}; - -const trackChanges = (): void => { - // Update the time if/when the document becomes hidden. - onHidden(({ timeStamp }) => { - firstHiddenTime = timeStamp; - }, true); -}; - -export const getVisibilityWatcher = (): { - readonly firstHiddenTime: number; -} => { - if (firstHiddenTime < 0) { - // If the document is hidden when this code runs, assume it was hidden - // since navigation start. This isn't a perfect heuristic, but it's the - // best we can do until an API is available to support querying past - // visibilityState. - firstHiddenTime = initHiddenTime(); - trackChanges(); - } - return { - get firstHiddenTime() { - return firstHiddenTime; - }, - }; -}; diff --git a/packages/tracing-internal/src/common/debug-build.ts b/packages/tracing-internal/src/common/debug-build.ts deleted file mode 100644 index 60aa50940582..000000000000 --- a/packages/tracing-internal/src/common/debug-build.ts +++ /dev/null @@ -1,8 +0,0 @@ -declare const __DEBUG_BUILD__: boolean; - -/** - * This serves as a build time flag that will be true by default, but false in non-debug builds or if users replace `__SENTRY_DEBUG__` in their generated code. - * - * ATTENTION: This constant must never cross package boundaries (i.e. be exported) to guarantee that it can be used for tree shaking. - */ -export const DEBUG_BUILD = __DEBUG_BUILD__; diff --git a/packages/tracing-internal/src/exports/index.ts b/packages/tracing-internal/src/exports/index.ts deleted file mode 100644 index b3a3e3a4b4ed..000000000000 --- a/packages/tracing-internal/src/exports/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { - hasTracingEnabled, - Transaction, -} from '@sentry/core'; -export { stripUrlQueryAndFragment, TRACEPARENT_REGEXP } from '@sentry/utils'; diff --git a/packages/tracing-internal/src/node/index.ts b/packages/tracing-internal/src/node/index.ts deleted file mode 100644 index eac5910c32c7..000000000000 --- a/packages/tracing-internal/src/node/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from '../exports'; - -export * from './integrations'; diff --git a/packages/tracing-internal/src/node/integrations/apollo.ts b/packages/tracing-internal/src/node/integrations/apollo.ts deleted file mode 100644 index 17ba0ab15482..000000000000 --- a/packages/tracing-internal/src/node/integrations/apollo.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core'; -import { arrayify, fill, loadModule, logger } from '@sentry/utils'; - -import { DEBUG_BUILD } from '../../common/debug-build'; -import type { LazyLoadedIntegration } from './lazy'; - -interface ApolloOptions { - useNestjs?: boolean; -} - -type ApolloResolverGroup = { - [key: string]: () => unknown; -}; - -type ApolloModelResolvers = { - [key: string]: ApolloResolverGroup; -}; - -type GraphQLModule = { - GraphQLFactory: { - prototype: { - create: (resolvers: ApolloModelResolvers[]) => unknown; - }; - }; -}; - -type ApolloModule = { - ApolloServerBase: { - prototype: { - constructSchema: (config: unknown) => unknown; - }; - }; -}; - -/** Tracing integration for Apollo */ -export class Apollo implements LazyLoadedIntegration { - /** - * @inheritDoc - */ - public static id: string = 'Apollo'; - - /** - * @inheritDoc - */ - public name: string; - - private readonly _useNest: boolean; - - private _module?: GraphQLModule & ApolloModule; - - /** - * @inheritDoc - */ - public constructor( - options: ApolloOptions = { - useNestjs: false, - }, - ) { - this.name = Apollo.id; - this._useNest = !!options.useNestjs; - } - - /** @inheritdoc */ - public loadDependency(): (GraphQLModule & ApolloModule) | undefined { - if (this._useNest) { - this._module = this._module || loadModule('@nestjs/graphql'); - } else { - this._module = this._module || loadModule('apollo-server-core'); - } - - return this._module; - } - - /** - * @inheritDoc - */ - public setupOnce(): void { - if (this._useNest) { - const pkg = this.loadDependency(); - - if (!pkg) { - DEBUG_BUILD && logger.error('Apollo-NestJS Integration was unable to require @nestjs/graphql package.'); - return; - } - - /** - * Iterate over resolvers of NestJS ResolversExplorerService before schemas are constructed. - */ - fill( - pkg.GraphQLFactory.prototype, - 'mergeWithSchema', - function (orig: (this: unknown, ...args: unknown[]) => unknown) { - return function ( - this: { resolversExplorerService: { explore: () => ApolloModelResolvers[] } }, - ...args: unknown[] - ) { - fill(this.resolversExplorerService, 'explore', function (orig: () => ApolloModelResolvers[]) { - return function (this: unknown) { - const resolvers = arrayify(orig.call(this)); - - const instrumentedResolvers = instrumentResolvers(resolvers); - - return instrumentedResolvers; - }; - }); - - return orig.call(this, ...args); - }; - }, - ); - } else { - const pkg = this.loadDependency(); - - if (!pkg) { - DEBUG_BUILD && logger.error('Apollo Integration was unable to require apollo-server-core package.'); - return; - } - - /** - * Iterate over resolvers of the ApolloServer instance before schemas are constructed. - */ - fill(pkg.ApolloServerBase.prototype, 'constructSchema', function (orig: (config: unknown) => unknown) { - return function (this: { - config: { resolvers?: ApolloModelResolvers[]; schema?: unknown; modules?: unknown }; - }) { - if (!this.config.resolvers) { - if (DEBUG_BUILD) { - if (this.config.schema) { - logger.warn( - 'Apollo integration is not able to trace `ApolloServer` instances constructed via `schema` property.' + - 'If you are using NestJS with Apollo, please use `Sentry.Integrations.Apollo({ useNestjs: true })` instead.', - ); - logger.warn(); - } else if (this.config.modules) { - logger.warn( - 'Apollo integration is not able to trace `ApolloServer` instances constructed via `modules` property.', - ); - } - - logger.error('Skipping tracing as no resolvers found on the `ApolloServer` instance.'); - } - - return orig.call(this); - } - - const resolvers = arrayify(this.config.resolvers); - - this.config.resolvers = instrumentResolvers(resolvers); - - return orig.call(this); - }; - }); - } - } -} - -function instrumentResolvers(resolvers: ApolloModelResolvers[]): ApolloModelResolvers[] { - return resolvers.map(model => { - Object.keys(model).forEach(resolverGroupName => { - Object.keys(model[resolverGroupName]).forEach(resolverName => { - if (typeof model[resolverGroupName][resolverName] !== 'function') { - return; - } - - wrapResolver(model, resolverGroupName, resolverName); - }); - }); - - return model; - }); -} - -/** - * Wrap a single resolver which can be a parent of other resolvers and/or db operations. - */ -function wrapResolver(model: ApolloModelResolvers, resolverGroupName: string, resolverName: string): void { - fill(model[resolverGroupName], resolverName, function (orig: () => unknown | Promise) { - return function (this: unknown, ...args: unknown[]) { - return startSpan( - { - onlyIfParent: true, - name: `${resolverGroupName}.${resolverName}`, - op: 'graphql.resolve', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.graphql.apollo', - }, - }, - () => { - return orig.call(this, ...args); - }, - ); - }; - }); -} diff --git a/packages/tracing-internal/src/node/integrations/express.ts b/packages/tracing-internal/src/node/integrations/express.ts deleted file mode 100644 index 26c82a2ef8dd..000000000000 --- a/packages/tracing-internal/src/node/integrations/express.ts +++ /dev/null @@ -1,583 +0,0 @@ -/* eslint-disable max-lines */ -import type { Transaction } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; -import { startInactiveSpan, withActiveSpan } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON } from '@sentry/core'; -import type { Integration, PolymorphicRequest } from '@sentry/types'; -import { - GLOBAL_OBJ, - extractPathForTransaction, - getNumberOfUrlSegments, - isRegExp, - logger, - stripUrlQueryAndFragment, -} from '@sentry/utils'; - -import { DEBUG_BUILD } from '../../common/debug-build'; - -type Method = - | 'all' - | 'get' - | 'post' - | 'put' - | 'delete' - | 'patch' - | 'options' - | 'head' - | 'checkout' - | 'copy' - | 'lock' - | 'merge' - | 'mkactivity' - | 'mkcol' - | 'move' - | 'm-search' - | 'notify' - | 'purge' - | 'report' - | 'search' - | 'subscribe' - | 'trace' - | 'unlock' - | 'unsubscribe' - | 'use'; - -type Router = { - [method in Method]: (...args: any) => any; // eslint-disable-line @typescript-eslint/no-explicit-any -}; - -/* Extend the PolymorphicRequest type with a patched parameter to build a reconstructed route */ -type PatchedRequest = PolymorphicRequest & { _reconstructedRoute?: string; _hasParameters?: boolean }; - -/* Types used for patching the express router prototype */ -type ExpressRouter = Router & { - _router?: ExpressRouter; - stack?: Layer[]; - lazyrouter?: () => void; - settings?: unknown; - process_params: ( - layer: Layer, - called: unknown, - req: PatchedRequest, - res: ExpressResponse, - done: () => void, - ) => unknown; -}; - -type Layer = { - match: (path: string) => boolean; - handle_request: (req: PatchedRequest, res: ExpressResponse, next: () => void) => void; - route?: { path: RouteType | RouteType[] }; - path?: string; - regexp?: RegExp; - keys?: { name: string | number; offset: number; optional: boolean }[]; -}; - -type RouteType = string | RegExp; - -interface ExpressResponse { - once(name: string, callback: () => void): void; -} - -/** - * Internal helper for `__sentry_transaction` - * @hidden - */ -interface SentryTracingResponse { - __sentry_transaction?: Transaction; -} - -/** - * Express integration - * - * Provides an request and error handler for Express framework as well as tracing capabilities - */ -export class Express implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'Express'; - - /** - * @inheritDoc - */ - public name: string; - - /** - * Express App instance - */ - private readonly _router?: Router; - private readonly _methods?: Method[]; - - /** - * @inheritDoc - */ - public constructor(options: { app?: Router; router?: Router; methods?: Method[] } = {}) { - this.name = Express.id; - this._router = options.router || options.app; - this._methods = (Array.isArray(options.methods) ? options.methods : []).concat('use'); - } - - /** - * @inheritDoc - */ - public setupOnce(): void { - if (!this._router) { - DEBUG_BUILD && logger.error('ExpressIntegration is missing an Express instance'); - return; - } - - instrumentMiddlewares(this._router, this._methods); - instrumentRouter(this._router as ExpressRouter); - } -} - -/** - * Wraps original middleware function in a tracing call, which stores the info about the call as a span, - * and finishes it once the middleware is done invoking. - * - * Express middlewares have 3 various forms, thus we have to take care of all of them: - * // sync - * app.use(function (req, res) { ... }) - * // async - * app.use(function (req, res, next) { ... }) - * // error handler - * app.use(function (err, req, res, next) { ... }) - * - * They all internally delegate to the `router[method]` of the given application instance. - */ -// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any -function wrap(fn: Function, method: Method): (...args: any[]) => void { - const arity = fn.length; - - switch (arity) { - case 2: { - return function (this: NodeJS.Global, req: unknown, res: ExpressResponse & SentryTracingResponse): void { - const transaction = res.__sentry_transaction; - if (transaction) { - const span = withActiveSpan(transaction, () => { - return startInactiveSpan({ - name: fn.name, - op: `middleware.express.${method}`, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.express', - }, - }); - }); - res.once('finish', () => { - span.end(); - }); - } - return fn.call(this, req, res); - }; - } - case 3: { - return function ( - this: NodeJS.Global, - req: unknown, - res: ExpressResponse & SentryTracingResponse, - next: () => void, - ): void { - const transaction = res.__sentry_transaction; - const span = transaction - ? withActiveSpan(transaction, () => { - return startInactiveSpan({ - name: fn.name, - op: `middleware.express.${method}`, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.express', - }, - }); - }) - : undefined; - fn.call(this, req, res, function (this: NodeJS.Global, ...args: unknown[]): void { - span?.end(); - next.call(this, ...args); - }); - }; - } - case 4: { - return function ( - this: NodeJS.Global, - err: Error, - req: Request, - res: Response & SentryTracingResponse, - next: () => void, - ): void { - const transaction = res.__sentry_transaction; - const span = transaction - ? withActiveSpan(transaction, () => { - return startInactiveSpan({ - name: fn.name, - op: `middleware.express.${method}`, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.express', - }, - }); - }) - : undefined; - fn.call(this, err, req, res, function (this: NodeJS.Global, ...args: unknown[]): void { - span?.end(); - next.call(this, ...args); - }); - }; - } - default: { - throw new Error(`Express middleware takes 2-4 arguments. Got: ${arity}`); - } - } -} - -/** - * Takes all the function arguments passed to the original `app` or `router` method, eg. `app.use` or `router.use` - * and wraps every function, as well as array of functions with a call to our `wrap` method. - * We have to take care of the arrays as well as iterate over all of the arguments, - * as `app.use` can accept middlewares in few various forms. - * - * app.use([], ) - * app.use([], , ...) - * app.use([], ...[]) - */ -function wrapMiddlewareArgs(args: unknown[], method: Method): unknown[] { - return args.map((arg: unknown) => { - if (typeof arg === 'function') { - return wrap(arg, method); - } - - if (Array.isArray(arg)) { - return arg.map((a: unknown) => { - if (typeof a === 'function') { - return wrap(a, method); - } - return a; - }); - } - - return arg; - }); -} - -/** - * Patches original router to utilize our tracing functionality - */ -function patchMiddleware(router: Router, method: Method): Router { - const originalCallback = router[method]; - - router[method] = function (...args: unknown[]): void { - return originalCallback.call(this, ...wrapMiddlewareArgs(args, method)); - }; - - return router; -} - -/** - * Patches original router methods - */ -function instrumentMiddlewares(router: Router, methods: Method[] = []): void { - methods.forEach((method: Method) => patchMiddleware(router, method)); -} - -/** - * Patches the prototype of Express.Router to accumulate the resolved route - * if a layer instance's `match` function was called and it returned a successful match. - * - * @see https://github.com/expressjs/express/blob/master/lib/router/index.js - * - * @param appOrRouter the router instance which can either be an app (i.e. top-level) or a (nested) router. - */ -function instrumentRouter(appOrRouter: ExpressRouter): void { - // This is how we can distinguish between app and routers - const isApp = 'settings' in appOrRouter; - - // In case the app's top-level router hasn't been initialized yet, we have to do it now - if (isApp && appOrRouter._router === undefined && appOrRouter.lazyrouter) { - appOrRouter.lazyrouter(); - } - - const router = isApp ? appOrRouter._router : appOrRouter; - - if (!router) { - /* - If we end up here, this means likely that this integration is used with Express 3 or Express 5. - For now, we don't support these versions (3 is very old and 5 is still in beta). To support Express 5, - we'd need to make more changes to the routing instrumentation because the router is no longer part of - the Express core package but maintained in its own package. The new router has different function - signatures and works slightly differently, demanding more changes than just taking the router from - `app.router` instead of `app._router`. - @see https://github.com/pillarjs/router - - TODO: Proper Express 5 support - */ - DEBUG_BUILD && logger.debug('Cannot instrument router for URL Parameterization (did not find a valid router).'); - DEBUG_BUILD && logger.debug('Routing instrumentation is currently only supported in Express 4.'); - return; - } - - const routerProto = Object.getPrototypeOf(router) as ExpressRouter; - - const originalProcessParams = routerProto.process_params; - routerProto.process_params = function process_params( - layer: Layer, - called: unknown, - req: PatchedRequest, - res: ExpressResponse & SentryTracingResponse, - done: () => unknown, - ) { - // Base case: We're in the first part of the URL (thus we start with the root '/') - if (!req._reconstructedRoute) { - req._reconstructedRoute = ''; - } - - // If the layer's partial route has params, is a regex or an array, the route is stored in layer.route. - const { layerRoutePath, isRegex, isArray, numExtraSegments }: LayerRoutePathInfo = getLayerRoutePathInfo(layer); - - if (layerRoutePath || isRegex || isArray) { - req._hasParameters = true; - } - - // Otherwise, the hardcoded path (i.e. a partial route without params) is stored in layer.path - let partialRoute; - - if (layerRoutePath) { - partialRoute = layerRoutePath; - } else { - /** - * prevent duplicate segment in _reconstructedRoute param if router match multiple routes before final path - * example: - * original url: /api/v1/1234 - * prevent: /api/api/v1/:userId - * router structure - * /api -> middleware - * /api/v1 -> middleware - * /1234 -> endpoint with param :userId - * final _reconstructedRoute is /api/v1/:userId - */ - partialRoute = preventDuplicateSegments(req.originalUrl, req._reconstructedRoute, layer.path) || ''; - } - - // Normalize the partial route so that it doesn't contain leading or trailing slashes - // and exclude empty or '*' wildcard routes. - // The exclusion of '*' routes is our best effort to not "pollute" the transaction name - // with interim handlers (e.g. ones that check authentication or do other middleware stuff). - // We want to end up with the parameterized URL of the incoming request without any extraneous path segments. - const finalPartialRoute = partialRoute - .split('/') - .filter(segment => segment.length > 0 && (isRegex || isArray || !segment.includes('*'))) - .join('/'); - - // If we found a valid partial URL, we append it to the reconstructed route - if (finalPartialRoute && finalPartialRoute.length > 0) { - // If the partial route is from a regex route, we append a '/' to close the regex - req._reconstructedRoute += `/${finalPartialRoute}${isRegex ? '/' : ''}`; - } - - // Now we check if we are in the "last" part of the route. We determine this by comparing the - // number of URL segments from the original URL to that of our reconstructed parameterized URL. - // If we've reached our final destination, we update the transaction name. - const urlLength = getNumberOfUrlSegments(stripUrlQueryAndFragment(req.originalUrl || '')) + numExtraSegments; - const routeLength = getNumberOfUrlSegments(req._reconstructedRoute); - - if (urlLength === routeLength) { - if (!req._hasParameters) { - if (req._reconstructedRoute !== req.originalUrl) { - req._reconstructedRoute = req.originalUrl ? stripUrlQueryAndFragment(req.originalUrl) : req.originalUrl; - } - } - - const transaction = res.__sentry_transaction; - const attributes = (transaction && spanToJSON(transaction).data) || {}; - if (transaction && attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'url') { - // If the request URL is '/' or empty, the reconstructed route will be empty. - // Therefore, we fall back to setting the final route to '/' in this case. - const finalRoute = req._reconstructedRoute || '/'; - - const [name, source] = extractPathForTransaction(req, { path: true, method: true, customRoute: finalRoute }); - transaction.updateName(name); - transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); - } - } - - return originalProcessParams.call(this, layer, called, req, res, done); - }; -} - -type LayerRoutePathInfo = { - layerRoutePath?: string; - isRegex: boolean; - isArray: boolean; - numExtraSegments: number; -}; - -/** - * Recreate layer.route.path from layer.regexp and layer.keys. - * Works until express.js used package path-to-regexp@0.1.7 - * or until layer.keys contain offset attribute - * - * @param layer the layer to extract the stringified route from - * - * @returns string in layer.route.path structure 'router/:pathParam' or undefined - */ -export const extractOriginalRoute = ( - path?: Layer['path'], - regexp?: Layer['regexp'], - keys?: Layer['keys'], -): string | undefined => { - if (!path || !regexp || !keys || Object.keys(keys).length === 0 || !keys[0]?.offset) { - return undefined; - } - - const orderedKeys = keys.sort((a, b) => a.offset - b.offset); - - // add d flag for getting indices from regexp result - // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- regexp comes from express.js - const pathRegex = new RegExp(regexp, `${regexp.flags}d`); - /** - * use custom type cause of TS error with missing indices in RegExpExecArray - */ - const execResult = pathRegex.exec(path) as (RegExpExecArray & { indices: [number, number][] }) | null; - - if (!execResult || !execResult.indices) { - return undefined; - } - /** - * remove first match from regex cause contain whole layer.path - */ - const [, ...paramIndices] = execResult.indices; - - if (paramIndices.length !== orderedKeys.length) { - return undefined; - } - let resultPath = path; - let indexShift = 0; - - /** - * iterate param matches from regexp.exec - */ - paramIndices.forEach((item: [number, number] | undefined, index: number) => { - /** check if offsets is define because in some cases regex d flag returns undefined */ - if (item) { - const [startOffset, endOffset] = item; - /** - * isolate part before param - */ - const substr1 = resultPath.substring(0, startOffset - indexShift); - /** - * define paramName as replacement in format :pathParam - */ - const replacement = `:${orderedKeys[index].name}`; - - /** - * isolate part after param - */ - const substr2 = resultPath.substring(endOffset - indexShift); - - /** - * recreate original path but with param replacement - */ - resultPath = substr1 + replacement + substr2; - - /** - * calculate new index shift after resultPath was modified - */ - indexShift = indexShift + (endOffset - startOffset - replacement.length); - } - }); - - return resultPath; -}; - -/** - * Extracts and stringifies the layer's route which can either be a string with parameters (`users/:id`), - * a RegEx (`/test/`) or an array of strings and regexes (`['/path1', /\/path[2-5]/, /path/:id]`). Additionally - * returns extra information about the route, such as if the route is defined as regex or as an array. - * - * @param layer the layer to extract the stringified route from - * - * @returns an object containing the stringified route, a flag determining if the route was a regex - * and the number of extra segments to the matched path that are additionally in the route, - * if the route was an array (defaults to 0). - */ -function getLayerRoutePathInfo(layer: Layer): LayerRoutePathInfo { - let lrp = layer.route?.path; - - const isRegex = isRegExp(lrp); - const isArray = Array.isArray(lrp); - - if (!lrp) { - // parse node.js major version - // Next.js will complain if we directly use `proces.versions` here because of edge runtime. - const [major] = (GLOBAL_OBJ as unknown as NodeJS.Global).process.versions.node.split('.').map(Number); - - // allow call extractOriginalRoute only if node version support Regex d flag, node 16+ - if (major >= 16) { - /** - * If lrp does not exist try to recreate original layer path from route regexp - */ - lrp = extractOriginalRoute(layer.path, layer.regexp, layer.keys); - } - } - - if (!lrp) { - return { isRegex, isArray, numExtraSegments: 0 }; - } - - const numExtraSegments = isArray - ? Math.max(getNumberOfArrayUrlSegments(lrp as RouteType[]) - getNumberOfUrlSegments(layer.path || ''), 0) - : 0; - - const layerRoutePath = getLayerRoutePathString(isArray, lrp); - - return { layerRoutePath, isRegex, isArray, numExtraSegments }; -} - -/** - * Returns the number of URL segments in an array of routes - * - * Example: ['/api/test', /\/api\/post[0-9]/, '/users/:id/details`] -> 7 - */ -function getNumberOfArrayUrlSegments(routesArray: RouteType[]): number { - return routesArray.reduce((accNumSegments: number, currentRoute: RouteType) => { - // array members can be a RegEx -> convert them toString - return accNumSegments + getNumberOfUrlSegments(currentRoute.toString()); - }, 0); -} - -/** - * Extracts and returns the stringified version of the layers route path - * Handles route arrays (by joining the paths together) as well as RegExp and normal - * string values (in the latter case the toString conversion is technically unnecessary but - * it doesn't hurt us either). - */ -function getLayerRoutePathString(isArray: boolean, lrp?: RouteType | RouteType[]): string | undefined { - if (isArray) { - return (lrp as RouteType[]).map(r => r.toString()).join(','); - } - return lrp && lrp.toString(); -} - -/** - * remove duplicate segment contain in layerPath against reconstructedRoute, - * and return only unique segment that can be added into reconstructedRoute - */ -export function preventDuplicateSegments( - originalUrl?: string, - reconstructedRoute?: string, - layerPath?: string, -): string | undefined { - // filter query params - const normalizeURL = stripUrlQueryAndFragment(originalUrl || ''); - const originalUrlSplit = normalizeURL?.split('/').filter(v => !!v); - let tempCounter = 0; - const currentOffset = reconstructedRoute?.split('/').filter(v => !!v).length || 0; - const result = layerPath - ?.split('/') - .filter(segment => { - if (originalUrlSplit?.[currentOffset + tempCounter] === segment) { - tempCounter += 1; - return true; - } - return false; - }) - .join('/'); - return result; -} diff --git a/packages/tracing-internal/src/node/integrations/graphql.ts b/packages/tracing-internal/src/node/integrations/graphql.ts deleted file mode 100644 index e61010e5953c..000000000000 --- a/packages/tracing-internal/src/node/integrations/graphql.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core'; -import { fill, loadModule, logger } from '@sentry/utils'; - -import { DEBUG_BUILD } from '../../common/debug-build'; -import type { LazyLoadedIntegration } from './lazy'; - -type GraphQLModule = { - [method: string]: (...args: unknown[]) => unknown; -}; - -/** Tracing integration for graphql package */ -export class GraphQL implements LazyLoadedIntegration { - /** - * @inheritDoc - */ - public static id: string = 'GraphQL'; - - /** - * @inheritDoc - */ - public name: string; - - private _module?: GraphQLModule; - - public constructor() { - this.name = GraphQL.id; - } - - /** @inheritdoc */ - public loadDependency(): GraphQLModule | undefined { - return (this._module = this._module || loadModule('graphql/execution/execute.js')); - } - - /** - * @inheritDoc - */ - public setupOnce(): void { - const pkg = this.loadDependency(); - - if (!pkg) { - DEBUG_BUILD && logger.error('GraphQL Integration was unable to require graphql/execution package.'); - return; - } - - fill(pkg, 'execute', function (orig: () => void | Promise) { - return function (this: unknown, ...args: unknown[]) { - return startSpan( - { - onlyIfParent: true, - name: 'execute', - op: 'graphql.execute', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.graphql.graphql', - }, - }, - () => { - return orig.call(this, ...args); - }, - ); - }; - }); - } -} diff --git a/packages/tracing-internal/src/node/integrations/index.ts b/packages/tracing-internal/src/node/integrations/index.ts deleted file mode 100644 index 0b69f4440f3a..000000000000 --- a/packages/tracing-internal/src/node/integrations/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { Express } from './express'; -export { Postgres } from './postgres'; -export { Mysql } from './mysql'; -export { Mongo } from './mongo'; -export { Prisma } from './prisma'; -export { GraphQL } from './graphql'; -export { Apollo } from './apollo'; -export * from './lazy'; diff --git a/packages/tracing-internal/src/node/integrations/lazy.ts b/packages/tracing-internal/src/node/integrations/lazy.ts deleted file mode 100644 index 635f5b082bee..000000000000 --- a/packages/tracing-internal/src/node/integrations/lazy.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { Integration, IntegrationClass } from '@sentry/types'; -import { dynamicRequire } from '@sentry/utils'; - -export interface LazyLoadedIntegration extends Integration { - /** - * Loads the integration's dependency and caches it so it doesn't have to be loaded again. - * - * If this returns undefined, the dependency could not be loaded. - */ - loadDependency(): T | undefined; -} - -export const lazyLoadedNodePerformanceMonitoringIntegrations: (() => LazyLoadedIntegration)[] = [ - () => { - const integration = dynamicRequire(module, './apollo') as { - Apollo: IntegrationClass; - }; - return new integration.Apollo(); - }, - () => { - const integration = dynamicRequire(module, './apollo') as { - Apollo: IntegrationClass; - }; - return new integration.Apollo({ useNestjs: true }); - }, - () => { - const integration = dynamicRequire(module, './graphql') as { - GraphQL: IntegrationClass; - }; - return new integration.GraphQL(); - }, - () => { - const integration = dynamicRequire(module, './mongo') as { - Mongo: IntegrationClass; - }; - return new integration.Mongo(); - }, - () => { - const integration = dynamicRequire(module, './mongo') as { - Mongo: IntegrationClass; - }; - return new integration.Mongo({ mongoose: true }); - }, - () => { - const integration = dynamicRequire(module, './mysql') as { - Mysql: IntegrationClass; - }; - return new integration.Mysql(); - }, - () => { - const integration = dynamicRequire(module, './postgres') as { - Postgres: IntegrationClass; - }; - return new integration.Postgres(); - }, -]; diff --git a/packages/tracing-internal/src/node/integrations/mongo.ts b/packages/tracing-internal/src/node/integrations/mongo.ts deleted file mode 100644 index b85e0f3ae03a..000000000000 --- a/packages/tracing-internal/src/node/integrations/mongo.ts +++ /dev/null @@ -1,272 +0,0 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; -import { getClient } from '@sentry/core'; -import type { SpanAttributes, StartSpanOptions } from '@sentry/types'; -import { fill, isThenable, loadModule, logger } from '@sentry/utils'; - -import { DEBUG_BUILD } from '../../common/debug-build'; -import type { LazyLoadedIntegration } from './lazy'; - -// This allows us to use the same array for both defaults options and the type itself. -// (note `as const` at the end to make it a union of string literal types (i.e. "a" | "b" | ... ) -// and not just a string[]) -type Operation = (typeof OPERATIONS)[number]; -const OPERATIONS = [ - 'aggregate', // aggregate(pipeline, options, callback) - 'bulkWrite', // bulkWrite(operations, options, callback) - 'countDocuments', // countDocuments(query, options, callback) - 'createIndex', // createIndex(fieldOrSpec, options, callback) - 'createIndexes', // createIndexes(indexSpecs, options, callback) - 'deleteMany', // deleteMany(filter, options, callback) - 'deleteOne', // deleteOne(filter, options, callback) - 'distinct', // distinct(key, query, options, callback) - 'drop', // drop(options, callback) - 'dropIndex', // dropIndex(indexName, options, callback) - 'dropIndexes', // dropIndexes(options, callback) - 'estimatedDocumentCount', // estimatedDocumentCount(options, callback) - 'find', // find(query, options, callback) - 'findOne', // findOne(query, options, callback) - 'findOneAndDelete', // findOneAndDelete(filter, options, callback) - 'findOneAndReplace', // findOneAndReplace(filter, replacement, options, callback) - 'findOneAndUpdate', // findOneAndUpdate(filter, update, options, callback) - 'indexes', // indexes(options, callback) - 'indexExists', // indexExists(indexes, options, callback) - 'indexInformation', // indexInformation(options, callback) - 'initializeOrderedBulkOp', // initializeOrderedBulkOp(options, callback) - 'insertMany', // insertMany(docs, options, callback) - 'insertOne', // insertOne(doc, options, callback) - 'isCapped', // isCapped(options, callback) - 'mapReduce', // mapReduce(map, reduce, options, callback) - 'options', // options(options, callback) - 'parallelCollectionScan', // parallelCollectionScan(options, callback) - 'rename', // rename(newName, options, callback) - 'replaceOne', // replaceOne(filter, doc, options, callback) - 'stats', // stats(options, callback) - 'updateMany', // updateMany(filter, update, options, callback) - 'updateOne', // updateOne(filter, update, options, callback) -] as const; - -// All of the operations above take `options` and `callback` as their final parameters, but some of them -// take additional parameters as well. For those operations, this is a map of -// { : [] }, as a way to know what to call the operation's -// positional arguments when we add them to the span's `data` object later -const OPERATION_SIGNATURES: { - [op in Operation]?: string[]; -} = { - // aggregate intentionally not included because `pipeline` arguments are too complex to serialize well - // see https://github.com/getsentry/sentry-javascript/pull/3102 - bulkWrite: ['operations'], - countDocuments: ['query'], - createIndex: ['fieldOrSpec'], - createIndexes: ['indexSpecs'], - deleteMany: ['filter'], - deleteOne: ['filter'], - distinct: ['key', 'query'], - dropIndex: ['indexName'], - find: ['query'], - findOne: ['query'], - findOneAndDelete: ['filter'], - findOneAndReplace: ['filter', 'replacement'], - findOneAndUpdate: ['filter', 'update'], - indexExists: ['indexes'], - insertMany: ['docs'], - insertOne: ['doc'], - mapReduce: ['map', 'reduce'], - rename: ['newName'], - replaceOne: ['filter', 'doc'], - updateMany: ['filter', 'update'], - updateOne: ['filter', 'update'], -}; - -interface MongoCollection { - collectionName: string; - dbName: string; - namespace: string; - prototype: { - [operation in Operation]: (...args: unknown[]) => unknown; - }; -} - -interface MongoOptions { - operations?: Operation[]; - describeOperations?: boolean | Operation[]; - useMongoose?: boolean; -} - -interface MongoCursor { - once(event: 'close', listener: () => void): void; -} - -function isCursor(maybeCursor: MongoCursor): maybeCursor is MongoCursor { - return maybeCursor && typeof maybeCursor === 'object' && maybeCursor.once && typeof maybeCursor.once === 'function'; -} - -type MongoModule = { Collection: MongoCollection }; - -/** Tracing integration for mongo package */ -export class Mongo implements LazyLoadedIntegration { - /** - * @inheritDoc - */ - public static id: string = 'Mongo'; - - /** - * @inheritDoc - */ - public name: string; - - private _operations: Operation[]; - private _describeOperations?: boolean | Operation[]; - private _useMongoose: boolean; - - private _module?: MongoModule; - - /** - * @inheritDoc - */ - public constructor(options: MongoOptions = {}) { - this.name = Mongo.id; - this._operations = Array.isArray(options.operations) ? options.operations : (OPERATIONS as unknown as Operation[]); - this._describeOperations = 'describeOperations' in options ? options.describeOperations : true; - this._useMongoose = !!options.useMongoose; - } - - /** @inheritdoc */ - public loadDependency(): MongoModule | undefined { - const moduleName = this._useMongoose ? 'mongoose' : 'mongodb'; - return (this._module = this._module || loadModule(moduleName)); - } - - /** - * @inheritDoc - */ - public setupOnce(): void { - const pkg = this.loadDependency(); - - if (!pkg) { - const moduleName = this._useMongoose ? 'mongoose' : 'mongodb'; - DEBUG_BUILD && logger.error(`Mongo Integration was unable to require \`${moduleName}\` package.`); - return; - } - - this._instrumentOperations(pkg.Collection, this._operations); - } - - /** - * Patches original collection methods - */ - private _instrumentOperations(collection: MongoCollection, operations: Operation[]): void { - operations.forEach((operation: Operation) => this._patchOperation(collection, operation)); - } - - /** - * Patches original collection to utilize our tracing functionality - */ - private _patchOperation(collection: MongoCollection, operation: Operation): void { - if (!(operation in collection.prototype)) return; - - const getSpanContext = this._getSpanContextFromOperationArguments.bind(this); - - fill(collection.prototype, operation, function (orig: () => void | Promise) { - return function (this: unknown, ...args: unknown[]) { - const lastArg = args[args.length - 1]; - - const client = getClient(); - - const sendDefaultPii = client?.getOptions().sendDefaultPii; - - // Check if the operation was passed a callback. (mapReduce requires a different check, as - // its (non-callback) arguments can also be functions.) - if (typeof lastArg !== 'function' || (operation === 'mapReduce' && args.length === 2)) { - const span = startInactiveSpan(getSpanContext(this, operation, args, sendDefaultPii)); - const maybePromiseOrCursor = orig.call(this, ...args); - - if (isThenable(maybePromiseOrCursor)) { - return maybePromiseOrCursor.then((res: unknown) => { - span?.end(); - return res; - }); - } - // If the operation returns a Cursor - // we need to attach a listener to it to finish the span when the cursor is closed. - else if (isCursor(maybePromiseOrCursor)) { - const cursor = maybePromiseOrCursor as MongoCursor; - - try { - cursor.once('close', () => { - span?.end(); - }); - } catch (e) { - // If the cursor is already closed, `once` will throw an error. In that case, we can - // finish the span immediately. - span?.end(); - } - - return cursor; - } else { - span?.end(); - return maybePromiseOrCursor; - } - } - - const span = startInactiveSpan(getSpanContext(this, operation, args.slice(0, -1))); - - return orig.call(this, ...args.slice(0, -1), function (err: Error, result: unknown) { - span?.end(); - lastArg(err, result); - }); - }; - }); - } - - /** - * Form a SpanContext based on the user input to a given operation. - */ - private _getSpanContextFromOperationArguments( - collection: MongoCollection, - operation: Operation, - args: unknown[], - sendDefaultPii: boolean | undefined = false, - ): StartSpanOptions { - const attributes: SpanAttributes = { - 'db.system': 'mongodb', - 'db.name': collection.dbName, - 'db.operation': operation, - 'db.mongodb.collection': collection.collectionName, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `${collection.collectionName}.${operation}`, - }; - const spanContext: StartSpanOptions = { - op: 'db', - name: operation, - attributes, - onlyIfParent: true, - }; - - // If the operation takes no arguments besides `options` and `callback`, or if argument - // collection is disabled for this operation, just return early. - const signature = OPERATION_SIGNATURES[operation]; - const shouldDescribe = Array.isArray(this._describeOperations) - ? this._describeOperations.includes(operation) - : this._describeOperations; - - if (!signature || !shouldDescribe || !sendDefaultPii) { - return spanContext; - } - - try { - // Special case for `mapReduce`, as the only one accepting functions as arguments. - if (operation === 'mapReduce') { - const [map, reduce] = args as { name?: string }[]; - attributes[signature[0]] = typeof map === 'string' ? map : map.name || ''; - attributes[signature[1]] = typeof reduce === 'string' ? reduce : reduce.name || ''; - } else { - for (let i = 0; i < signature.length; i++) { - attributes[`db.mongodb.${signature[i]}`] = JSON.stringify(args[i]); - } - } - } catch (_oO) { - // no-empty - } - - return spanContext; - } -} diff --git a/packages/tracing-internal/src/node/integrations/mysql.ts b/packages/tracing-internal/src/node/integrations/mysql.ts deleted file mode 100644 index c62a6db0028f..000000000000 --- a/packages/tracing-internal/src/node/integrations/mysql.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; -import type { Span } from '@sentry/types'; -import { fill, loadModule, logger } from '@sentry/utils'; - -import { DEBUG_BUILD } from '../../common/debug-build'; -import type { LazyLoadedIntegration } from './lazy'; - -interface MysqlConnection { - prototype: { - connect: () => void; - }; - createQuery: () => void; -} - -interface MysqlConnectionConfig { - host: string; - port: number; - user: string; -} - -/** Tracing integration for node-mysql package */ -export class Mysql implements LazyLoadedIntegration { - /** - * @inheritDoc - */ - public static id: string = 'Mysql'; - - /** - * @inheritDoc - */ - public name: string; - - private _module?: MysqlConnection; - - public constructor() { - this.name = Mysql.id; - } - - /** @inheritdoc */ - public loadDependency(): MysqlConnection | undefined { - return (this._module = this._module || loadModule('mysql/lib/Connection.js')); - } - - /** - * @inheritDoc - */ - public setupOnce(): void { - const pkg = this.loadDependency(); - - if (!pkg) { - DEBUG_BUILD && logger.error('Mysql Integration was unable to require `mysql` package.'); - return; - } - - let mySqlConfig: MysqlConnectionConfig | undefined = undefined; - - try { - pkg.prototype.connect = new Proxy(pkg.prototype.connect, { - apply(wrappingTarget, thisArg: { config: MysqlConnectionConfig }, args) { - if (!mySqlConfig) { - mySqlConfig = thisArg.config; - } - return wrappingTarget.apply(thisArg, args); - }, - }); - } catch (e) { - DEBUG_BUILD && logger.error('Mysql Integration was unable to instrument `mysql` config.'); - } - - function spanDataFromConfig(): Record { - if (!mySqlConfig) { - return {}; - } - return { - 'server.address': mySqlConfig.host, - 'server.port': mySqlConfig.port, - 'db.user': mySqlConfig.user, - }; - } - - function finishSpan(span: Span | undefined): void { - if (!span) { - return; - } - - const data = spanDataFromConfig(); - Object.keys(data).forEach(key => { - span.setAttribute(key, data[key]); - }); - - span.end(); - } - - // The original function will have one of these signatures: - // function (callback) => void - // function (options, callback) => void - // function (options, values, callback) => void - fill(pkg, 'createQuery', function (orig: () => void) { - return function (this: unknown, options: unknown, values: unknown, callback: unknown) { - const span = startInactiveSpan({ - onlyIfParent: true, - name: typeof options === 'string' ? options : (options as { sql: string }).sql, - op: 'db', - attributes: { - 'db.system': 'mysql', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.mysql', - }, - }); - - if (typeof callback === 'function') { - return orig.call(this, options, values, function (err: Error, result: unknown, fields: unknown) { - finishSpan(span); - callback(err, result, fields); - }); - } - - if (typeof values === 'function') { - return orig.call(this, options, function (err: Error, result: unknown, fields: unknown) { - finishSpan(span); - values(err, result, fields); - }); - } - - // streaming, no callback! - const query = orig.call(this, options, values) as { on: (event: string, callback: () => void) => void }; - - query.on('end', () => { - finishSpan(span); - }); - - return query; - }; - }); - } -} diff --git a/packages/tracing-internal/src/node/integrations/postgres.ts b/packages/tracing-internal/src/node/integrations/postgres.ts deleted file mode 100644 index 30f94e41998c..000000000000 --- a/packages/tracing-internal/src/node/integrations/postgres.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; -import type { SpanAttributes } from '@sentry/types'; -import { fill, isThenable, loadModule, logger } from '@sentry/utils'; - -import { DEBUG_BUILD } from '../../common/debug-build'; -import type { LazyLoadedIntegration } from './lazy'; - -type PgClientQuery = ( - config: unknown, - values?: unknown, - callback?: (err: unknown, result: unknown) => void, -) => void | Promise; - -interface PgClient { - prototype: { - query: PgClientQuery; - }; -} - -interface PgClientThis { - database?: string; - host?: string; - port?: number; - user?: string; -} - -interface PgOptions { - usePgNative?: boolean; - /** - * Supply your postgres module directly, instead of having Sentry attempt automatic resolution. - * Use this if you (a) use a module that's not `pg`, or (b) use a bundler that breaks resolution (e.g. esbuild). - * - * Usage: - * ``` - * import pg from 'pg'; - * - * Sentry.init({ - * integrations: [new Sentry.Integrations.Postgres({ module: pg })], - * }); - * ``` - */ - module?: PGModule; -} - -type PGModule = { Client: PgClient; native: { Client: PgClient } | null }; - -/** Tracing integration for node-postgres package */ -export class Postgres implements LazyLoadedIntegration { - /** - * @inheritDoc - */ - public static id: string = 'Postgres'; - - /** - * @inheritDoc - */ - public name: string; - - private _usePgNative: boolean; - - private _module?: PGModule; - - public constructor(options: PgOptions = {}) { - this.name = Postgres.id; - this._usePgNative = !!options.usePgNative; - this._module = options.module; - } - - /** @inheritdoc */ - public loadDependency(): PGModule | undefined { - return (this._module = this._module || loadModule('pg')); - } - - /** - * @inheritDoc - */ - public setupOnce(): void { - const pkg = this.loadDependency(); - - if (!pkg) { - DEBUG_BUILD && logger.error('Postgres Integration was unable to require `pg` package.'); - return; - } - - const Client = this._usePgNative ? pkg.native?.Client : pkg.Client; - - if (!Client) { - DEBUG_BUILD && logger.error("Postgres Integration was unable to access 'pg-native' bindings."); - return; - } - - /** - * function (query, callback) => void - * function (query, params, callback) => void - * function (query) => Promise - * function (query, params) => Promise - * function (pg.Cursor) => pg.Cursor - */ - fill(Client.prototype, 'query', function (orig: PgClientQuery) { - return function (this: PgClientThis, config: unknown, values: unknown, callback: unknown) { - const attributes: SpanAttributes = { - 'db.system': 'postgresql', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.postgres', - }; - - try { - if (this.database) { - attributes['db.name'] = this.database; - } - if (this.host) { - attributes['server.address'] = this.host; - } - if (this.port) { - attributes['server.port'] = this.port; - } - if (this.user) { - attributes['db.user'] = this.user; - } - } catch (e) { - // ignore - } - - const span = startInactiveSpan({ - onlyIfParent: true, - name: typeof config === 'string' ? config : (config as { text: string }).text, - op: 'db', - attributes, - }); - - if (typeof callback === 'function') { - return orig.call(this, config, values, function (err: Error, result: unknown) { - span?.end(); - callback(err, result); - }); - } - - if (typeof values === 'function') { - return orig.call(this, config, function (err: Error, result: unknown) { - span?.end(); - values(err, result); - }); - } - - const rv = typeof values !== 'undefined' ? orig.call(this, config, values) : orig.call(this, config); - - if (isThenable(rv)) { - return rv.then((res: unknown) => { - span?.end(); - return res; - }); - } - - span?.end(); - return rv; - }; - }); - } -} diff --git a/packages/tracing-internal/src/node/integrations/prisma.ts b/packages/tracing-internal/src/node/integrations/prisma.ts deleted file mode 100644 index cb9db58e2a21..000000000000 --- a/packages/tracing-internal/src/node/integrations/prisma.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core'; -import type { Integration } from '@sentry/types'; -import { addNonEnumerableProperty, logger } from '@sentry/utils'; - -import { DEBUG_BUILD } from '../../common/debug-build'; - -type PrismaAction = - | 'findUnique' - | 'findMany' - | 'findFirst' - | 'create' - | 'createMany' - | 'update' - | 'updateMany' - | 'upsert' - | 'delete' - | 'deleteMany' - | 'executeRaw' - | 'queryRaw' - | 'aggregate' - | 'count' - | 'runCommandRaw'; - -interface PrismaMiddlewareParams { - model?: unknown; - action: PrismaAction; - args: unknown; - dataPath: string[]; - runInTransaction: boolean; -} - -type PrismaMiddleware = ( - params: PrismaMiddlewareParams, - next: (params: PrismaMiddlewareParams) => Promise, -) => Promise; - -interface PrismaClient { - _sentryInstrumented?: boolean; - _engineConfig?: { - activeProvider?: string; - clientVersion?: string; - }; - $use: (cb: PrismaMiddleware) => void; -} - -function isValidPrismaClient(possibleClient: unknown): possibleClient is PrismaClient { - return !!possibleClient && !!(possibleClient as PrismaClient)['$use']; -} - -/** Tracing integration for @prisma/client package */ -export class Prisma implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'Prisma'; - - /** - * @inheritDoc - */ - public name: string; - - /** - * @inheritDoc - */ - public constructor(options: { client?: unknown } = {}) { - this.name = Prisma.id; - - // We instrument the PrismaClient inside the constructor and not inside `setupOnce` because in some cases of server-side - // bundling (Next.js) multiple Prisma clients can be instantiated, even though users don't intend to. When instrumenting - // in setupOnce we can only ever instrument one client. - // https://github.com/getsentry/sentry-javascript/issues/7216#issuecomment-1602375012 - // In the future we might explore providing a dedicated PrismaClient middleware instead of this hack. - if (isValidPrismaClient(options.client) && !options.client._sentryInstrumented) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - addNonEnumerableProperty(options.client as any, '_sentryInstrumented', true); - - const clientData: Record = {}; - try { - const engineConfig = (options.client as PrismaClient)._engineConfig; - if (engineConfig) { - const { activeProvider, clientVersion } = engineConfig; - if (activeProvider) { - clientData['db.system'] = activeProvider; - } - if (clientVersion) { - clientData['db.prisma.version'] = clientVersion; - } - } - } catch (e) { - // ignore - } - - options.client.$use((params, next: (params: PrismaMiddlewareParams) => Promise) => { - const action = params.action; - const model = params.model; - - return startSpan( - { - name: model ? `${model} ${action}` : action, - onlyIfParent: true, - op: 'db.prisma', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.prisma', - ...clientData, - 'db.operation': action, - }, - }, - () => next(params), - ); - }); - } else { - DEBUG_BUILD && - logger.warn('Unsupported Prisma client provided to PrismaIntegration. Provided client:', options.client); - } - } - - /** - * @inheritDoc - */ - public setupOnce(): void { - // Noop - here for backwards compatibility - } -} diff --git a/packages/tracing-internal/test/node/express.test.ts b/packages/tracing-internal/test/node/express.test.ts deleted file mode 100644 index 1631971d9863..000000000000 --- a/packages/tracing-internal/test/node/express.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { extractOriginalRoute, preventDuplicateSegments } from '../../src/node/integrations/express'; - -/** - * prevent duplicate segment in _reconstructedRoute param if router match multiple routes before final path - * example: - * original url: /api/v1/1234 - * prevent: /api/api/v1/:userId - * router structure - * /api -> middleware - * /api/v1 -> middleware - * /1234 -> endpoint with param :userId - * final _reconstructedRoute is /api/v1/:userId - */ -describe('unit Test for preventDuplicateSegments', () => { - it('should return api segment', () => { - const originalUrl = '/api/v1/1234'; - const reconstructedRoute = ''; - const layerPath = '/api'; - const result = preventDuplicateSegments(originalUrl, reconstructedRoute, layerPath); - expect(result).toBe('api'); - }); - - it('should prevent duplicate segment api', () => { - const originalUrl = '/api/v1/1234'; - const reconstructedRoute = '/api'; - const layerPath = '/api/v1'; - const result = preventDuplicateSegments(originalUrl, reconstructedRoute, layerPath); - expect(result).toBe('v1'); - }); - - it('should prevent duplicate segment v1', () => { - const originalUrl = '/api/v1/1234'; - const reconstructedRoute = '/api/v1'; - const layerPath = '/v1/1234'; - const result1 = preventDuplicateSegments(originalUrl, reconstructedRoute, layerPath); - expect(result1).toBe('1234'); - }); - - it('should prevent duplicate segment v1 originalUrl with query param without trailing slash', () => { - const originalUrl = '/api/v1/1234?queryParam=123'; - const reconstructedRoute = '/api/v1'; - const layerPath = '/v1/1234'; - const result1 = preventDuplicateSegments(originalUrl, reconstructedRoute, layerPath); - expect(result1).toBe('1234'); - }); - - it('should prevent duplicate segment v1 originalUrl with query param with trailing slash', () => { - const originalUrl = '/api/v1/1234/?queryParam=123'; - const reconstructedRoute = '/api/v1'; - const layerPath = '/v1/1234'; - const result1 = preventDuplicateSegments(originalUrl, reconstructedRoute, layerPath); - expect(result1).toBe('1234'); - }); -}); -describe('preventDuplicateSegments should handle empty input gracefully', () => { - it('Empty input values', () => { - expect(preventDuplicateSegments()).toBeUndefined(); - }); - - it('Empty originalUrl', () => { - expect(preventDuplicateSegments('', '/api/v1/1234', '/api/api/v1/1234')).toBe(''); - }); - - it('Empty reconstructedRoute', () => { - expect(preventDuplicateSegments('/api/v1/1234', '', '/api/api/v1/1234')).toBe('api/v1/1234'); - }); - - it('Empty layerPath', () => { - expect(preventDuplicateSegments('/api/v1/1234', '/api/v1/1234', '')).toBe(''); - }); -}); - -// parse node.js major version -const [major] = process.versions.node.split('.').map(Number); -// Test this funciton only if node is 16+ because regex d flag is support from node 16+ -if (major >= 16) { - describe('extractOriginalRoute', () => { - it('should return undefined if path, regexp, or keys are missing', () => { - expect(extractOriginalRoute('/example')).toBeUndefined(); - expect(extractOriginalRoute('/example', /test/)).toBeUndefined(); - }); - - it('should return undefined if keys do not contain an offset property', () => { - const path = '/example'; - const regex = /example/; - const key = { name: 'param1', offset: 0, optional: false }; - expect(extractOriginalRoute(path, regex, [key])).toBeUndefined(); - }); - - it('should return the original route path when valid inputs are provided', () => { - const path = '/router/123'; - const regex = /^\/router\/(\d+)$/; - const keys = [{ name: 'pathParam', offset: 8, optional: false }]; - expect(extractOriginalRoute(path, regex, keys)).toBe('/router/:pathParam'); - }); - - it('should handle multiple parameters in the route', () => { - const path = '/user/42/profile/username'; - const regex = /^\/user\/(\d+)\/profile\/(\w+)$/; - const keys = [ - { name: 'userId', offset: 6, optional: false }, - { name: 'username', offset: 17, optional: false }, - ]; - expect(extractOriginalRoute(path, regex, keys)).toBe('/user/:userId/profile/:username'); - }); - - it('should handle complex regex scheme extract from array of routes', () => { - const path1 = '/@fs/*'; - const path2 = '/@vite/client'; - const path3 = '/@react-refresh'; - const path4 = '/manifest.json'; - - const regex = - /(?:^\/manifest\.json\/?(?=\/|$)|^\/@vite\/client\/?(?=\/|$)|^\/@react-refresh\/?(?=\/|$)|^\/src\/(.*)\/?(?=\/|$)|^\/vite\/(.*)\/?(?=\/|$)|^\/node_modules\/(.*)\/?(?=\/|$)|^\/@fs\/(.*)\/?(?=\/|$)|^\/@vite-plugin-checker-runtime\/?(?=\/|$)|^\/?$\/?(?=\/|$)|^\/home\/?$\/?(?=\/|$)|^\/login\/?(?=\/|$))/; - const keys = [ - { name: 0, offset: 8, optional: false }, - { name: 0, offset: 8, optional: false }, - { name: 0, offset: 9, optional: false }, - { name: 0, offset: 17, optional: false }, - ]; - - expect(extractOriginalRoute(path1, regex, keys)).toBe('/@fs/:0'); - expect(extractOriginalRoute(path2, regex, keys)).toBe('/@vite/client'); - expect(extractOriginalRoute(path3, regex, keys)).toBe('/@react-refresh'); - expect(extractOriginalRoute(path4, regex, keys)).toBe('/manifest.json'); - }); - }); -} diff --git a/packages/types/src/hub.ts b/packages/types/src/hub.ts index 77fec1dc9a69..53ecac91b507 100644 --- a/packages/types/src/hub.ts +++ b/packages/types/src/hub.ts @@ -14,16 +14,6 @@ import type { User } from './user'; * working in case we have a version conflict. */ export interface Hub { - /** - * Checks if this hub's version is older than the given version. - * - * @param version A version number to compare to. - * @return True if the given version is newer; otherwise false. - * - * @deprecated This will be removed in v8. - */ - isOlderThan(version: number): boolean; - /** * This binds the given client to the current scope. * @param client An SDK client (client) instance. @@ -32,31 +22,6 @@ export interface Hub { */ bindClient(client?: Client): void; - /** - * Create a new scope to store context information. - * - * The scope will be layered on top of the current one. It is isolated, i.e. all - * breadcrumbs and context information added to this scope will be removed once - * the scope ends. Be sure to always remove this scope with {@link this.popScope} - * when the operation finishes or throws. - * - * @returns Scope, the new cloned scope - * - * @deprecated Use `withScope` instead. - */ - pushScope(): Scope; - - /** - * Removes a previously pushed scope from the stack. - * - * This restores the state before the scope was pushed. All breadcrumbs and - * context information added since the last call to {@link this.pushScope} are - * discarded. - * - * @deprecated Use `withScope` instead. - */ - popScope(): boolean; - /** * Creates a new scope with and executes the given operation within. * The scope is automatically removed once the operation @@ -234,13 +199,4 @@ export interface Hub { * @deprecated Use top-level `captureSession` instead. */ captureSession(endSession?: boolean): void; - - /** - * Returns if default PII should be sent to Sentry and propagated in outgoing requests - * when Tracing is used. - * - * @deprecated Use top-level `getClient().getOptions().sendDefaultPii` instead. This function - * only unnecessarily increased API surface but only wrapped accessing the option. - */ - shouldSendDefaultPii(): boolean; } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 9b8009de4fc4..e1ceb7e7e4e2 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -90,7 +90,7 @@ export type { export type { SeverityLevel } from './severity'; export type { Span, - SpanContext, + SentrySpanArguments, SpanOrigin, SpanAttributeValue, SpanAttributes, @@ -101,6 +101,7 @@ export type { MetricSummary, } from './span'; export type { SpanStatus } from './spanStatus'; +export type { TimedEvent } from './timedEvent'; export type { StackFrame } from './stackframe'; export type { Stacktrace, StackParser, StackLineParser, StackLineParserFn } from './stacktrace'; export type { PropagationContext, TracePropagationTargets } from './tracing'; @@ -109,9 +110,6 @@ export type { CustomSamplingContext, SamplingContext, TraceparentData, - Transaction, - TransactionContext, - TransactionMetadata, TransactionSource, } from './transaction'; export type { @@ -155,3 +153,4 @@ export type { MetricInstance, } from './metrics'; export type { ParameterizedString } from './parameterize'; +export type { ViewHierarchyData, ViewHierarchyWindow } from './view-hierarchy'; diff --git a/packages/types/src/instrument.ts b/packages/types/src/instrument.ts index 5c42c8cebf27..f0b239e86b14 100644 --- a/packages/types/src/instrument.ts +++ b/packages/types/src/instrument.ts @@ -29,10 +29,6 @@ export interface SentryXhrData { } export interface HandlerDataXhr { - /** - * @deprecated This property will be removed in v8. - */ - args: [string, string]; xhr: SentryWrappedXMLHttpRequest; startTimestamp?: number; endTimestamp?: number; diff --git a/packages/types/src/options.ts b/packages/types/src/options.ts index 3f8c55e84949..c9c95140902d 100644 --- a/packages/types/src/options.ts +++ b/packages/types/src/options.ts @@ -1,5 +1,5 @@ import type { Breadcrumb, BreadcrumbHint } from './breadcrumb'; -import type { ErrorEvent, Event, EventHint, TransactionEvent } from './event'; +import type { ErrorEvent, EventHint, TransactionEvent } from './event'; import type { Integration } from './integration'; import type { CaptureContext } from './scope'; import type { SdkMetadata } from './sdkmetadata'; @@ -255,7 +255,6 @@ export interface ClientOptions number | boolean; - // TODO (v8): Narrow the response type to `ErrorEvent` - this is technically a breaking change. /** * An event-processing callback for error and message events, guaranteed to be invoked after all other event * processors, which allows an event to be modified or dropped. @@ -267,9 +266,8 @@ export interface ClientOptions PromiseLike | Event | null; + beforeSend?: (event: ErrorEvent, hint: EventHint) => PromiseLike | ErrorEvent | null; - // TODO (v8): Narrow the response type to `TransactionEvent` - this is technically a breaking change. /** * An event-processing callback for transaction events, guaranteed to be invoked after all other event * processors. This allows an event to be modified or dropped before it's sent. @@ -281,7 +279,10 @@ export interface ClientOptions PromiseLike | Event | null; + beforeSendTransaction?: ( + event: TransactionEvent, + hint: EventHint, + ) => PromiseLike | TransactionEvent | null; /** * A callback invoked when adding a breadcrumb, allowing to optionally modify diff --git a/packages/types/src/scope.ts b/packages/types/src/scope.ts index 641a77dd080c..83a03de45125 100644 --- a/packages/types/src/scope.ts +++ b/packages/types/src/scope.ts @@ -10,7 +10,6 @@ import type { RequestSession, Session } from './session'; import type { SeverityLevel } from './severity'; import type { Span } from './span'; import type { PropagationContext } from './tracing'; -import type { Transaction } from './transaction'; import type { User } from './user'; /** JSDocs */ @@ -145,12 +144,6 @@ export interface Scope { */ setContext(name: string, context: Context | null): this; - /** - * Returns the `Transaction` attached to the scope (if there is one). - * @deprecated You should not rely on the transaction, but just use `startSpan()` APIs instead. - */ - getTransaction(): Transaction | undefined; - /** * Returns the `Session` if there is one */ @@ -206,11 +199,6 @@ export interface Scope { */ addAttachment(attachment: Attachment): this; - /** - * Returns an array of attachments on the scope - */ - getAttachments(): Attachment[]; - /** * Clears attachments from the scope */ diff --git a/packages/types/src/span.ts b/packages/types/src/span.ts index 76b8ba18fcdc..9b787576b011 100644 --- a/packages/types/src/span.ts +++ b/packages/types/src/span.ts @@ -116,7 +116,7 @@ export interface SpanContextData { spanId: string; /** - * Only true if the SpanContext was propagated from a remote parent. + * Only true if the SentrySpanArguments was propagated from a remote parent. */ isRemote?: boolean | undefined; @@ -138,7 +138,7 @@ export interface SpanContextData { * Interface holding all properties that can be set on a Span on creation. * This is only used for the legacy span/transaction creation and will go away in v8. */ -export interface SpanContext { +export interface SentrySpanArguments { /** * Human-readable identifier for the span. */ @@ -169,12 +169,6 @@ export interface SpanContext { */ traceId?: string | undefined; - /** - * Data of the Span. - * @deprecated Pass `attributes` instead. - */ - data?: { [key: string]: any }; - /** * Attributes of the Span. */ @@ -233,4 +227,9 @@ export interface Span { * This will return false if tracing is disabled, this span was not sampled or if the span is already finished. */ isRecording(): boolean; + + /** + * Adds an event to the Span. + */ + addEvent(name: string, attributesOrStartTime?: SpanAttributes | SpanTimeInput, startTime?: SpanTimeInput): this; } diff --git a/packages/types/src/timedEvent.ts b/packages/types/src/timedEvent.ts new file mode 100644 index 000000000000..2139d37f2b83 --- /dev/null +++ b/packages/types/src/timedEvent.ts @@ -0,0 +1,7 @@ +import type { SpanAttributes, SpanTimeInput } from './span'; + +export interface TimedEvent { + name: string; + time: SpanTimeInput; + attributes?: SpanAttributes; +} diff --git a/packages/types/src/transaction.ts b/packages/types/src/transaction.ts index a5ebe5d29ecb..8f5109b44ba0 100644 --- a/packages/types/src/transaction.ts +++ b/packages/types/src/transaction.ts @@ -1,38 +1,5 @@ -import type { Context } from './context'; -import type { DynamicSamplingContext } from './envelope'; -import type { MeasurementUnit } from './measurement'; import type { ExtractedNodeRequestData, WorkerLocation } from './misc'; -import type { PolymorphicRequest } from './polymorphics'; -import type { Span, SpanAttributes, SpanContext } from './span'; - -/** - * Interface holding Transaction-specific properties - */ -export interface TransactionContext extends SpanContext { - /** - * Human-readable identifier for the transaction - */ - name: string; - - /** - * If true, sets the end timestamp of the transaction to the highest timestamp of child spans, trimming - * the duration of the transaction. This is useful to discard extra time in the transaction that is not - * accounted for in child spans, like what happens in the idle transaction Tracing integration, where we finish the - * transaction after a given "idle time" and we don't want this "idle time" to be part of the transaction. - */ - trimEnd?: boolean | undefined; - - /** - * If this transaction has a parent, the parent's sampling decision - */ - parentSampled?: boolean | undefined; - - /** - * Metadata associated with the transaction, for internal SDK use. - * @deprecated Use attributes or store data on the scope instead. - */ - metadata?: Partial; -} +import type { SpanAttributes } from './span'; /** * Data pulled from a `sentry-trace` header @@ -54,78 +21,6 @@ export interface TraceparentData { parentSampled?: boolean | undefined; } -/** - * Transaction "Class", inherits Span only has `setName` - */ -export interface Transaction extends Omit, Span { - /** - * @inheritDoc - */ - startTimestamp: number; - - /** - * Data for the transaction. - * @deprecated Use `getSpanAttributes(transaction)` instead. - */ - data: { [key: string]: any }; - - /** - * Attributes for the transaction. - * @deprecated Use `getSpanAttributes(transaction)` instead. - */ - attributes: SpanAttributes; - - /** - * Metadata about the transaction. - * @deprecated Use attributes or store data on the scope instead. - */ - metadata: TransactionMetadata; - - /** - * Set the context of a transaction event. - * @deprecated Use either `.setAttribute()`, or set the context on the scope before creating the transaction. - */ - setContext(key: string, context: Context): void; - - /** - * Set observed measurement for this transaction. - * - * @param name Name of the measurement - * @param value Value of the measurement - * @param unit Unit of the measurement. (Defaults to an empty string) - * - * @deprecated Use top-level `setMeasurement()` instead. - */ - setMeasurement(name: string, value: number, unit: MeasurementUnit): void; - - /** - * Returns the current transaction properties as a `TransactionContext`. - * @deprecated Use `toJSON()` or access the fields directly instead. - */ - toContext(): TransactionContext; - - /** - * Set metadata for this transaction. - * @deprecated Use attributes or store data on the scope instead. - */ - setMetadata(newMetadata: Partial): void; - - /** - * Return the current Dynamic Sampling Context of this transaction - * - * @deprecated Use top-level `getDynamicSamplingContextFromSpan` instead. - */ - getDynamicSamplingContext(): Partial; - - /** - * Creates a new `Span` while setting the current `Span.id` as `parentSpanId`. - * Also the `sampled` decision will be inherited. - * - * @deprecated Use `startSpan()`, `startSpanManual()` or `startInactiveSpan()` instead. - */ - startChild(spanContext?: Pick>): Span; -} - /** * Context data passed by the user when starting a transaction, to be used by the tracesSampler method. */ @@ -140,9 +35,13 @@ export interface CustomSamplingContext { */ export interface SamplingContext extends CustomSamplingContext { /** - * Context data with which transaction being sampled was created + * Context data with which transaction being sampled was created. + * @deprecated This is duplicate data and will be removed eventually. */ - transactionContext: TransactionContext; + transactionContext: { + name: string; + parentSampled?: boolean | undefined; + }; /** * Sampling decision from the parent transaction, if any. @@ -159,27 +58,12 @@ export interface SamplingContext extends CustomSamplingContext { * Object representing the incoming request to a node server. Passed by default when using the TracingHandler. */ request?: ExtractedNodeRequestData; -} -export interface TransactionMetadata { - /** - * The sample rate used when sampling this transaction. - * @deprecated Use `SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE` attribute instead. - */ - sampleRate?: number; - - /** - * The Dynamic Sampling Context of a transaction. If provided during transaction creation, its Dynamic Sampling - * Context Will be frozen - */ - dynamicSamplingContext?: Partial; - - /** For transactions tracing server-side request handling, the request being tracked. */ - request?: PolymorphicRequest; + /** The name of the span being sampled. */ + name: string; - /** For transactions tracing server-side request handling, the path of the request being tracked. */ - /** TODO: If we rm -rf `instrumentServer`, this can go, too */ - requestPath?: string; + /** Initial attributes that have been passed to the span being sampled. */ + attributes?: SpanAttributes; } /** diff --git a/packages/types/src/view-hierarchy.ts b/packages/types/src/view-hierarchy.ts new file mode 100644 index 000000000000..a066bfbe42e6 --- /dev/null +++ b/packages/types/src/view-hierarchy.ts @@ -0,0 +1,18 @@ +export type ViewHierarchyWindow = { + alpha: number; + height: number; + type: string; + visible: boolean; + width: number; + x: number; + y: number; + z?: number; + children?: ViewHierarchyWindow[]; + depth?: number; + identifier?: string; +} & Record; + +export type ViewHierarchyData = { + rendering_system: string; + windows: ViewHierarchyWindow[]; +}; diff --git a/packages/utils/rollup.npm.config.mjs b/packages/utils/rollup.npm.config.mjs index fd61fbf7c62c..d28a7a6f54a0 100644 --- a/packages/utils/rollup.npm.config.mjs +++ b/packages/utils/rollup.npm.config.mjs @@ -6,8 +6,11 @@ export default makeNPMConfigVariants( output: { // set exports to 'named' or 'auto' so that rollup doesn't warn exports: 'named', - // set preserveModules to false because we want to bundle everything into one file. - preserveModules: false, + // set preserveModules to true because we don't want to bundle everything into one file. + preserveModules: + process.env.SENTRY_BUILD_PRESERVE_MODULES === undefined + ? true + : Boolean(process.env.SENTRY_BUILD_PRESERVE_MODULES), }, }, }), diff --git a/packages/utils/src/baggage.ts b/packages/utils/src/baggage.ts index 25210d02bbc4..b0e506b8938a 100644 --- a/packages/utils/src/baggage.ts +++ b/packages/utils/src/baggage.ts @@ -28,31 +28,10 @@ export function baggageHeaderToDynamicSamplingContext( // Very liberal definition of what any incoming header might look like baggageHeader: string | string[] | number | null | undefined | boolean, ): Partial | undefined { - if (!isString(baggageHeader) && !Array.isArray(baggageHeader)) { - return undefined; - } - - // Intermediary object to store baggage key value pairs of incoming baggage headers on. - // It is later used to read Sentry-DSC-values from. - let baggageObject: Readonly> = {}; + const baggageObject = parseBaggageHeader(baggageHeader); - if (Array.isArray(baggageHeader)) { - // Combine all baggage headers into one object containing the baggage values so we can later read the Sentry-DSC-values from it - baggageObject = baggageHeader.reduce>((acc, curr) => { - const currBaggageObject = baggageHeaderToObject(curr); - for (const key of Object.keys(currBaggageObject)) { - acc[key] = currBaggageObject[key]; - } - return acc; - }, {}); - } else { - // Return undefined if baggage header is an empty string (technically an empty baggage header is not spec conform but - // this is how we choose to handle it) - if (!baggageHeader) { - return undefined; - } - - baggageObject = baggageHeaderToObject(baggageHeader); + if (!baggageObject) { + return undefined; } // Read all "sentry-" prefixed values out of the baggage object and put it onto a dynamic sampling context object. @@ -104,6 +83,30 @@ export function dynamicSamplingContextToSentryBaggageHeader( return objectToBaggageHeader(sentryPrefixedDSC); } +/** + * Take a baggage header and parse it into an object. + */ +export function parseBaggageHeader( + baggageHeader: string | string[] | number | null | undefined | boolean, +): Record | undefined { + if (!baggageHeader || (!isString(baggageHeader) && !Array.isArray(baggageHeader))) { + return undefined; + } + + if (Array.isArray(baggageHeader)) { + // Combine all baggage headers into one object containing the baggage values so we can later read the Sentry-DSC-values from it + return baggageHeader.reduce>((acc, curr) => { + const currBaggageObject = baggageHeaderToObject(curr); + for (const key of Object.keys(currBaggageObject)) { + acc[key] = currBaggageObject[key]; + } + return acc; + }, {}); + } + + return baggageHeaderToObject(baggageHeader); +} + /** * Will parse a baggage header, which is a simple key-value map, into a flat object. * diff --git a/packages/utils/src/browser.ts b/packages/utils/src/browser.ts index 91a62eaafced..371e7e96c8c2 100644 --- a/packages/utils/src/browser.ts +++ b/packages/utils/src/browser.ts @@ -1,8 +1,7 @@ import { isString } from './is'; -import { getGlobalObject } from './worldwide'; +import { GLOBAL_OBJ } from './worldwide'; -// eslint-disable-next-line deprecation/deprecation -const WINDOW = getGlobalObject(); +const WINDOW = GLOBAL_OBJ as unknown as Window; const DEFAULT_MAX_STRING_LENGTH = 80; @@ -89,8 +88,13 @@ function _htmlElementAsString(el: unknown, keyAttrs?: string[]): string { // @ts-expect-error WINDOW has HTMLElement if (WINDOW.HTMLElement) { // If using the component name annotation plugin, this value may be available on the DOM node - if (elem instanceof HTMLElement && elem.dataset && elem.dataset['sentryComponent']) { - return elem.dataset['sentryComponent']; + if (elem instanceof HTMLElement && elem.dataset) { + if (elem.dataset['sentryComponent']) { + return elem.dataset['sentryComponent']; + } + if (elem.dataset['sentryElement']) { + return elem.dataset['sentryElement']; + } } } @@ -167,8 +171,8 @@ export function getDomElement(selector: string): E | null { /** * Given a DOM element, traverses up the tree until it finds the first ancestor node - * that has the `data-sentry-component` attribute. This attribute is added at build-time - * by projects that have the component name annotation plugin installed. + * that has the `data-sentry-component` or `data-sentry-element` attribute with `data-sentry-component` taking + * precendence. This attribute is added at build-time by projects that have the component name annotation plugin installed. * * @returns a string representation of the component for the provided DOM element, or `null` if not found */ @@ -185,8 +189,13 @@ export function getComponentName(elem: unknown): string | null { return null; } - if (currentElem instanceof HTMLElement && currentElem.dataset['sentryComponent']) { - return currentElem.dataset['sentryComponent']; + if (currentElem instanceof HTMLElement) { + if (currentElem.dataset['sentryComponent']) { + return currentElem.dataset['sentryComponent']; + } + if (currentElem.dataset['sentryElement']) { + return currentElem.dataset['sentryElement']; + } } currentElem = currentElem.parentNode; diff --git a/packages/utils/src/instrument/xhr.ts b/packages/utils/src/instrument/xhr.ts index bef77659b3ee..b00300fd553a 100644 --- a/packages/utils/src/instrument/xhr.ts +++ b/packages/utils/src/instrument/xhr.ts @@ -78,7 +78,6 @@ export function instrumentXHR(): void { } const handlerData: HandlerDataXhr = { - args: [method, url], endTimestamp: Date.now(), startTimestamp, xhr: this, @@ -132,7 +131,6 @@ export function instrumentXHR(): void { } const handlerData: HandlerDataXhr = { - args: [sentryXhrData.method, sentryXhrData.url], startTimestamp: Date.now(), xhr: this, }; diff --git a/packages/utils/src/is.ts b/packages/utils/src/is.ts index fea0402053cc..13ae0edce489 100644 --- a/packages/utils/src/is.ts +++ b/packages/utils/src/is.ts @@ -168,17 +168,6 @@ export function isSyntheticEvent(wat: unknown): boolean { return isPlainObject(wat) && 'nativeEvent' in wat && 'preventDefault' in wat && 'stopPropagation' in wat; } -/** - * Checks whether given value is NaN - * {@link isNaN}. - * - * @param wat A value to be checked. - * @returns A boolean representing the result. - */ -export function isNaN(wat: unknown): boolean { - return typeof wat === 'number' && wat !== wat; -} - /** * Checks whether given value's type is an instance of provided constructor. * {@link isInstanceOf}. diff --git a/packages/utils/src/normalize.ts b/packages/utils/src/normalize.ts index 064ea0ee934e..d86af9561c89 100644 --- a/packages/utils/src/normalize.ts +++ b/packages/utils/src/normalize.ts @@ -1,6 +1,6 @@ import type { Primitive } from '@sentry/types'; -import { isNaN, isSyntheticEvent, isVueViewModel } from './is'; +import { isSyntheticEvent, isVueViewModel } from './is'; import type { MemoFunc } from './memo'; import { memoBuilder } from './memo'; import { convertToPlainObject } from './object'; @@ -81,7 +81,7 @@ function visit( // Get the simple cases out of the way first if ( value == null || // this matches null and undefined -> eqeq not eqeqeq - (['number', 'boolean', 'string'].includes(typeof value) && !isNaN(value)) + (['number', 'boolean', 'string'].includes(typeof value) && !Number.isNaN(value)) ) { return value as Primitive; } diff --git a/packages/utils/src/requestdata.ts b/packages/utils/src/requestdata.ts index f6a4129fac2a..807bc0541c2c 100644 --- a/packages/utils/src/requestdata.ts +++ b/packages/utils/src/requestdata.ts @@ -2,7 +2,6 @@ import type { Event, ExtractedNodeRequestData, PolymorphicRequest, - Transaction, TransactionSource, WebFetchHeaders, WebFetchRequest, @@ -24,17 +23,6 @@ const DEFAULT_INCLUDES = { const DEFAULT_REQUEST_INCLUDES = ['cookies', 'data', 'headers', 'method', 'query_string', 'url']; export const DEFAULT_USER_INCLUDES = ['id', 'username', 'email']; -type InjectedNodeDeps = { - cookie: { - parse: (cookieStr: string) => Record; - }; - url: { - parse: (urlStr: string) => { - query: string | null; - }; - }; -}; - /** * Options deciding what parts of the request to use when enhancing an event */ @@ -62,45 +50,6 @@ export type AddRequestDataToEventOptions = { export type TransactionNamingScheme = 'path' | 'methodPath' | 'handler'; -/** - * Sets parameterized route as transaction name e.g.: `GET /users/:id` - * Also adds more context data on the transaction from the request - */ -export function addRequestDataToTransaction( - transaction: Transaction | undefined, - req: PolymorphicRequest, - // TODO(v8): Remove this parameter in v8 - _deps?: InjectedNodeDeps, -): void { - if (!transaction) return; - - // TODO(v8): SEMANTIC_ATTRIBUTE_SENTRY_SOURCE is in core, align this once we merge utils & core - // eslint-disable-next-line deprecation/deprecation - if (!transaction.attributes['sentry.source'] || transaction.attributes['sentry.source'] === 'url') { - // Attempt to grab a parameterized route off of the request - const [name, source] = extractPathForTransaction(req, { path: true, method: true }); - transaction.updateName(name); - // TODO(v8): SEMANTIC_ATTRIBUTE_SENTRY_SOURCE is in core, align this once we merge utils & core - transaction.setAttribute('sentry.source', source); - } - transaction.setAttribute('url', req.originalUrl || req.url); - if (req.baseUrl) { - transaction.setAttribute('baseUrl', req.baseUrl); - } - - const query = extractQueryParams(req); - if (typeof query === 'string') { - transaction.setAttribute('query', query); - } else if (query) { - Object.keys(query).forEach(key => { - const val = query[key]; - if (typeof val === 'string' || typeof val === 'number') { - transaction.setAttribute(`query.${key}`, val); - } - }); - } -} - /** * Extracts a complete and parameterized path from the request object and uses it to construct transaction name. * If the parameterized transaction name cannot be extracted, we fall back to the raw URL. diff --git a/packages/utils/src/supports.ts b/packages/utils/src/supports.ts index 01d4627d2892..2931e9b72c4c 100644 --- a/packages/utils/src/supports.ts +++ b/packages/utils/src/supports.ts @@ -1,9 +1,8 @@ import { DEBUG_BUILD } from './debug-build'; import { logger } from './logger'; -import { getGlobalObject } from './worldwide'; +import { GLOBAL_OBJ } from './worldwide'; -// eslint-disable-next-line deprecation/deprecation -const WINDOW = getGlobalObject(); +const WINDOW = GLOBAL_OBJ as unknown as Window; declare const EdgeRuntime: string | undefined; diff --git a/packages/utils/src/vendor/supportsHistory.ts b/packages/utils/src/vendor/supportsHistory.ts index 35af156eb96f..c0e2f61f0d4f 100644 --- a/packages/utils/src/vendor/supportsHistory.ts +++ b/packages/utils/src/vendor/supportsHistory.ts @@ -21,10 +21,9 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import { getGlobalObject } from '../worldwide'; +import { GLOBAL_OBJ } from '../worldwide'; -// eslint-disable-next-line deprecation/deprecation -const WINDOW = getGlobalObject(); +const WINDOW = GLOBAL_OBJ as unknown as Window; /** * Tells whether current environment supports History API diff --git a/packages/utils/src/worldwide.ts b/packages/utils/src/worldwide.ts index 6ac98ac0f6ae..3f67a044a607 100644 --- a/packages/utils/src/worldwide.ts +++ b/packages/utils/src/worldwide.ts @@ -12,7 +12,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { Client, Integration, MetricsAggregator, Scope } from '@sentry/types'; +import type { Client, MetricsAggregator, Scope } from '@sentry/types'; import type { SdkSource } from './env'; @@ -20,9 +20,7 @@ import type { SdkSource } from './env'; export interface InternalGlobal { navigator?: { userAgent?: string }; console: Console; - Sentry?: { - Integrations?: Integration[]; - }; + Sentry?: any; onerror?: { (event: object | string, source?: string, lineno?: number, colno?: number, error?: Error): any; __SENTRY_INSTRUMENTED__?: true; @@ -70,53 +68,8 @@ export interface InternalGlobal { _sentryModuleMetadata?: Record; } -// The code below for 'isGlobalObj' and 'GLOBAL_OBJ' was copied from core-js before modification -// https://github.com/zloirock/core-js/blob/1b944df55282cdc99c90db5f49eb0b6eda2cc0a3/packages/core-js/internals/global.js -// core-js has the following licence: -// -// Copyright (c) 2014-2022 Denis Pushkarev -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -/** Returns 'obj' if it's the global object, otherwise returns undefined */ -function isGlobalObj(obj: { Math?: Math }): any | undefined { - return obj && obj.Math == Math ? obj : undefined; -} - /** Get's the global object for the current JavaScript runtime */ -export const GLOBAL_OBJ: InternalGlobal = - (typeof globalThis == 'object' && isGlobalObj(globalThis)) || - // eslint-disable-next-line no-restricted-globals - (typeof window == 'object' && isGlobalObj(window)) || - (typeof self == 'object' && isGlobalObj(self)) || - (typeof global == 'object' && isGlobalObj(global)) || - (function (this: any) { - return this; - })() || - {}; - -/** - * @deprecated Use GLOBAL_OBJ instead or WINDOW from @sentry/browser. This will be removed in v8 - */ -export function getGlobalObject(): T & InternalGlobal { - return GLOBAL_OBJ as T & InternalGlobal; -} +export const GLOBAL_OBJ = globalThis as unknown as InternalGlobal; /** * Returns a global singleton contained in the global `__SENTRY__` object. diff --git a/packages/utils/test/is.test.ts b/packages/utils/test/is.test.ts index c97c751cb6d3..1ccfc2cd1754 100644 --- a/packages/utils/test/is.test.ts +++ b/packages/utils/test/is.test.ts @@ -4,7 +4,6 @@ import { isError, isErrorEvent, isInstanceOf, - isNaN, isPlainObject, isPrimitive, isThenable, @@ -122,21 +121,6 @@ describe('isInstanceOf()', () => { }); }); -describe('isNaN()', () => { - test('should work as advertised', () => { - expect(isNaN(NaN)).toEqual(true); - - expect(isNaN(null)).toEqual(false); - expect(isNaN(true)).toEqual(false); - expect(isNaN('foo')).toEqual(false); - expect(isNaN(42)).toEqual(false); - expect(isNaN({})).toEqual(false); - expect(isNaN([])).toEqual(false); - expect(isNaN(new Error('foo'))).toEqual(false); - expect(isNaN(new Date())).toEqual(false); - }); -}); - describe('isVueViewModel()', () => { test('should work as advertised', () => { expect(isVueViewModel({ _isVue: true })).toEqual(true); diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index 67b9393af3eb..58fdc6bdd9c4 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -13,7 +13,6 @@ export type { StackFrame, Stacktrace, Thread, - Transaction, User, } from '@sentry/types'; export type { AddRequestDataToEventOptions } from '@sentry/utils'; @@ -69,6 +68,7 @@ export { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + trpcMiddleware, } from '@sentry/core'; export { VercelEdgeClient } from './client'; diff --git a/packages/vercel-edge/src/types.ts b/packages/vercel-edge/src/types.ts index ec91431d8e60..7544820c75a3 100644 --- a/packages/vercel-edge/src/types.ts +++ b/packages/vercel-edge/src/types.ts @@ -33,24 +33,6 @@ export interface BaseVercelEdgeOptions { * */ clientClass?: typeof VercelEdgeClient; - // TODO (v8): Remove this in v8 - /** - * @deprecated Moved to constructor options of the `Http` and `Undici` integration. - * @example - * ```js - * Sentry.init({ - * integrations: [ - * new Sentry.Integrations.Http({ - * tracing: { - * shouldCreateSpanForRequest: (url: string) => false, - * } - * }); - * ], - * }); - * ``` - */ - shouldCreateSpanForRequest?(this: void, url: string): boolean; - /** Callback that is executed when a fatal global error occurs. */ onFatalError?(this: void, error: Error): void; } diff --git a/packages/vue/src/router.ts b/packages/vue/src/router.ts index 9d8e04bf75b2..ba42c8ba9fb5 100644 --- a/packages/vue/src/router.ts +++ b/packages/vue/src/router.ts @@ -3,10 +3,11 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, getActiveSpan, + getCurrentScope, getRootSpan, spanToJSON, } from '@sentry/core'; -import type { Span, SpanAttributes, TransactionContext, TransactionSource } from '@sentry/types'; +import type { Span, SpanAttributes, StartSpanOptions, TransactionSource } from '@sentry/types'; // The following type is an intersection of the Route type from VueRouter v2, v3, and v4. // This is not great, but kinda necessary to make it work with all versions at the same time. @@ -47,7 +48,7 @@ export function instrumentVueRouter( instrumentPageLoad: boolean; instrumentNavigation: boolean; }, - startNavigationSpanFn: (context: TransactionContext) => void, + startNavigationSpanFn: (context: StartSpanOptions) => void, ): void { router.onError(error => captureException(error, { mechanism: { handled: false } })); @@ -77,22 +78,24 @@ export function instrumentVueRouter( } // Determine a name for the routing transaction and where that name came from - let transactionName: string = to.path; + let spanName: string = to.path; let transactionSource: TransactionSource = 'url'; if (to.name && options.routeLabel !== 'path') { - transactionName = to.name.toString(); + spanName = to.name.toString(); transactionSource = 'custom'; } else if (to.matched[0] && to.matched[0].path) { - transactionName = to.matched[0].path; + spanName = to.matched[0].path; transactionSource = 'route'; } + getCurrentScope().setTransactionName(spanName); + if (options.instrumentPageLoad && isPageLoadNavigation) { const activeRootSpan = getActiveRootSpan(); if (activeRootSpan) { const existingAttributes = spanToJSON(activeRootSpan).data || {}; if (existingAttributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] !== 'custom') { - activeRootSpan.updateName(transactionName); + activeRootSpan.updateName(spanName); activeRootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, transactionSource); } // Set router attributes on the existing pageload transaction @@ -108,7 +111,7 @@ export function instrumentVueRouter( attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = transactionSource; attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = 'auto.navigation.vue'; startNavigationSpanFn({ - name: transactionName, + name: spanName, op: 'navigation', attributes, }); diff --git a/packages/vue/test/router.test.ts b/packages/vue/test/router.test.ts index 1a27e84961b1..8ff42d49e2b9 100644 --- a/packages/vue/test/router.test.ts +++ b/packages/vue/test/router.test.ts @@ -276,6 +276,35 @@ describe('instrumentVueRouter()', () => { expect(mockRootSpan.name).toEqual('customTxnName'); }); + it("updates the scope's `transactionName` when a route is resolved", () => { + const mockStartSpan = jest.fn().mockImplementation(_ => { + return {}; + }); + + const scopeSetTransactionNameSpy = jest.fn(); + + // @ts-expect-error - only creating a partial scope but that's fine + jest.spyOn(SentryCore, 'getCurrentScope').mockImplementation(() => ({ + setTransactionName: scopeSetTransactionNameSpy, + })); + + instrumentVueRouter( + mockVueRouter, + { routeLabel: 'name', instrumentPageLoad: true, instrumentNavigation: true }, + mockStartSpan, + ); + + const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0][0]; + + const from = testRoutes['initialPageloadRoute']; + const to = testRoutes['normalRoute1']; + + beforeEachCallback(to, from, mockNext); + + expect(scopeSetTransactionNameSpy).toHaveBeenCalledTimes(1); + expect(scopeSetTransactionNameSpy).toHaveBeenCalledWith('/books/:bookId/chapter/:chapterId'); + }); + test.each([ [false, 0], [true, 1], diff --git a/scripts/node-unit-tests.ts b/scripts/node-unit-tests.ts index 94802525b4dd..bf46320334df 100644 --- a/scripts/node-unit-tests.ts +++ b/scripts/node-unit-tests.ts @@ -17,6 +17,7 @@ const DEFAULT_SKIP_TESTS_PACKAGES = [ '@sentry/angular', '@sentry/svelte', '@sentry/profiling-node', + '@sentry-internal/browser-utils', '@sentry-internal/replay', '@sentry-internal/replay-canvas', '@sentry-internal/replay-worker', diff --git a/yarn.lock b/yarn.lock index a855ba24ac3a..64bc3a06b8bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2631,10 +2631,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.0.tgz#509621cca4e67caf0d18561a0c56f8b70237472f" integrity sha512-fGFDEctNh0CcSwsiRPxiaqX0P5rq+AqE0SRhYGZ4PX46Lg1FNR6oCxJghf8YgY0WQEgQuh3lErUFE4KxLeRmmw== -"@esbuild/android-arm64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz#cf91e86df127aa3d141744edafcba0abdc577d23" - integrity sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg== +"@esbuild/aix-ppc64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537" + integrity sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g== "@esbuild/android-arm64@0.18.20": version "0.18.20" @@ -2656,16 +2656,16 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.0.tgz#109a6fdc4a2783fc26193d2687827045d8fef5ab" integrity sha512-aVpnM4lURNkp0D3qPoAzSG92VXStYmoVPOgXveAUoQBWRSuQzt51yvSju29J6AHPmwY1BjH49uR29oyfH1ra8Q== +"@esbuild/android-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9" + integrity sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg== + "@esbuild/android-arm@0.15.18": version "0.15.18" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.15.18.tgz#266d40b8fdcf87962df8af05b76219bc786b4f80" integrity sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw== -"@esbuild/android-arm@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.16.17.tgz#025b6246d3f68b7bbaa97069144fb5fb70f2fff2" - integrity sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw== - "@esbuild/android-arm@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz#fedb265bc3a589c84cc11f810804f234947c3682" @@ -2686,10 +2686,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.0.tgz#1397a2c54c476c4799f9b9073550ede496c94ba5" integrity sha512-3bMAfInvByLHfJwYPJRlpTeaQA75n8C/QKpEaiS4HrFWFiJlNI0vzq/zCjBrhAYcPyVPG7Eo9dMrcQXuqmNk5g== -"@esbuild/android-x64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.16.17.tgz#c820e0fef982f99a85c4b8bfdd582835f04cd96e" - integrity sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ== +"@esbuild/android-arm@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995" + integrity sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w== "@esbuild/android-x64@0.18.20": version "0.18.20" @@ -2711,10 +2711,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.0.tgz#2b615abefb50dc0a70ac313971102f4ce2fdb3ca" integrity sha512-uK7wAnlRvjkCPzh8jJ+QejFyrP8ObKuR5cBIsQZ+qbMunwR8sbd8krmMbxTLSrDhiPZaJYKQAU5Y3iMDcZPhyQ== -"@esbuild/darwin-arm64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz#edef4487af6b21afabba7be5132c26d22379b220" - integrity sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w== +"@esbuild/android-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98" + integrity sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg== "@esbuild/darwin-arm64@0.18.20": version "0.18.20" @@ -2736,10 +2736,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.0.tgz#5c122ed799eb0c35b9d571097f77254964c276a2" integrity sha512-AjEcivGAlPs3UAcJedMa9qYg9eSfU6FnGHJjT8s346HSKkrcWlYezGE8VaO2xKfvvlZkgAhyvl06OJOxiMgOYQ== -"@esbuild/darwin-x64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz#42829168730071c41ef0d028d8319eea0e2904b4" - integrity sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg== +"@esbuild/darwin-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz#6e8517a045ddd86ae30c6608c8475ebc0c4000bb" + integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA== "@esbuild/darwin-x64@0.18.20": version "0.18.20" @@ -2761,10 +2761,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.0.tgz#9561d277002ba8caf1524f209de2b22e93d170c1" integrity sha512-bsgTPoyYDnPv8ER0HqnJggXK6RyFy4PH4rtsId0V7Efa90u2+EifxytE9pZnsDgExgkARy24WUQGv9irVbTvIw== -"@esbuild/freebsd-arm64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz#1f4af488bfc7e9ced04207034d398e793b570a27" - integrity sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw== +"@esbuild/darwin-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz#90ed098e1f9dd8a9381695b207e1cff45540a0d0" + integrity sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA== "@esbuild/freebsd-arm64@0.18.20": version "0.18.20" @@ -2786,10 +2786,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.0.tgz#84178986a3138e8500d17cc380044868176dd821" integrity sha512-kQ7jYdlKS335mpGbMW5tEe3IrQFIok9r84EM3PXB8qBFJPSc6dpWfrtsC/y1pyrz82xfUIn5ZrnSHQQsd6jebQ== -"@esbuild/freebsd-x64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz#636306f19e9bc981e06aa1d777302dad8fddaf72" - integrity sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug== +"@esbuild/freebsd-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911" + integrity sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw== "@esbuild/freebsd-x64@0.18.20": version "0.18.20" @@ -2811,10 +2811,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.0.tgz#3f9ce53344af2f08d178551cd475629147324a83" integrity sha512-uG8B0WSepMRsBNVXAQcHf9+Ko/Tr+XqmK7Ptel9HVmnykupXdS4J7ovSQUIi0tQGIndhbqWLaIL/qO/cWhXKyQ== -"@esbuild/linux-arm64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz#a003f7ff237c501e095d4f3a09e58fc7b25a4aca" - integrity sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g== +"@esbuild/freebsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c" + integrity sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw== "@esbuild/linux-arm64@0.18.20": version "0.18.20" @@ -2836,10 +2836,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.0.tgz#24efa685515689df4ecbc13031fa0a9dda910a11" integrity sha512-uTtyYAP5veqi2z9b6Gr0NUoNv9F/rOzI8tOD5jKcCvRUn7T60Bb+42NDBCWNhMjkQzI0qqwXkQGo1SY41G52nw== -"@esbuild/linux-arm@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz#b591e6a59d9c4fe0eeadd4874b157ab78cf5f196" - integrity sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ== +"@esbuild/linux-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5" + integrity sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A== "@esbuild/linux-arm@0.18.20": version "0.18.20" @@ -2861,10 +2861,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.0.tgz#6b586a488e02e9b073a75a957f2952b3b6e87b4c" integrity sha512-2ezuhdiZw8vuHf1HKSf4TIk80naTbP9At7sOqZmdVwvvMyuoDiZB49YZKLsLOfKIr77+I40dWpHVeY5JHpIEIg== -"@esbuild/linux-ia32@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz#24333a11027ef46a18f57019450a5188918e2a54" - integrity sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg== +"@esbuild/linux-arm@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c" + integrity sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg== "@esbuild/linux-ia32@0.18.20": version "0.18.20" @@ -2886,6 +2886,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.0.tgz#84ce7864f762708dcebc1b123898a397dea13624" integrity sha512-c88wwtfs8tTffPaoJ+SQn3y+lKtgTzyjkD8NgsyCtCmtoIC8RDL7PrJU05an/e9VuAke6eJqGkoMhJK1RY6z4w== +"@esbuild/linux-ia32@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa" + integrity sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig== + "@esbuild/linux-loong64@0.15.18": version "0.15.18" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz#128b76ecb9be48b60cf5cfc1c63a4f00691a3239" @@ -2896,11 +2901,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.15.5.tgz#91aef76d332cdc7c8942b600fa2307f3387e6f82" integrity sha512-UHkDFCfSGTuXq08oQltXxSZmH1TXyWsL+4QhZDWvvLl6mEJQqk3u7/wq1LjhrrAXYIllaTtRSzUXl4Olkf2J8A== -"@esbuild/linux-loong64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz#d5ad459d41ed42bbd4d005256b31882ec52227d8" - integrity sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ== - "@esbuild/linux-loong64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz#e6fccb7aac178dd2ffb9860465ac89d7f23b977d" @@ -2921,10 +2921,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.0.tgz#1922f571f4cae1958e3ad29439c563f7d4fd9037" integrity sha512-lR2rr/128/6svngnVta6JN4gxSXle/yZEZL3o4XZ6esOqhyR4wsKyfu6qXAL04S4S5CgGfG+GYZnjFd4YiG3Aw== -"@esbuild/linux-mips64el@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz#4e5967a665c38360b0a8205594377d4dcf9c3726" - integrity sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw== +"@esbuild/linux-loong64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5" + integrity sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ== "@esbuild/linux-mips64el@0.18.20": version "0.18.20" @@ -2946,10 +2946,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.0.tgz#7ca1bd9df3f874d18dbf46af009aebdb881188fe" integrity sha512-9Sycc+1uUsDnJCelDf6ZNqgZQoK1mJvFtqf2MUz4ujTxGhvCWw+4chYfDLPepMEvVL9PDwn6HrXad5yOrNzIsQ== -"@esbuild/linux-ppc64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz#206443a02eb568f9fdf0b438fbd47d26e735afc8" - integrity sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g== +"@esbuild/linux-mips64el@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa" + integrity sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA== "@esbuild/linux-ppc64@0.18.20": version "0.18.20" @@ -2971,10 +2971,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.0.tgz#8f95baf05f9486343bceeb683703875d698708a4" integrity sha512-CoWSaaAXOZd+CjbUTdXIJE/t7Oz+4g90A3VBCHLbfuc5yUQU/nFDLOzQsN0cdxgXd97lYW/psIIBdjzQIwTBGw== -"@esbuild/linux-riscv64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz#c351e433d009bf256e798ad048152c8d76da2fc9" - integrity sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw== +"@esbuild/linux-ppc64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20" + integrity sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg== "@esbuild/linux-riscv64@0.18.20": version "0.18.20" @@ -2996,10 +2996,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.0.tgz#ca63b921d5fe315e28610deb0c195e79b1a262ca" integrity sha512-mlb1hg/eYRJUpv8h/x+4ShgoNLL8wgZ64SUr26KwglTYnwAWjkhR2GpoKftDbPOCnodA9t4Y/b68H4J9XmmPzA== -"@esbuild/linux-s390x@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz#661f271e5d59615b84b6801d1c2123ad13d9bd87" - integrity sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w== +"@esbuild/linux-riscv64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300" + integrity sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg== "@esbuild/linux-s390x@0.18.20": version "0.18.20" @@ -3021,10 +3021,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.0.tgz#cb3d069f47dc202f785c997175f2307531371ef8" integrity sha512-fgf9ubb53xSnOBqyvWEY6ukBNRl1mVX1srPNu06B6mNsNK20JfH6xV6jECzrQ69/VMiTLvHMicQR/PgTOgqJUQ== -"@esbuild/linux-x64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz#e4ba18e8b149a89c982351443a377c723762b85f" - integrity sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw== +"@esbuild/linux-s390x@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685" + integrity sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ== "@esbuild/linux-x64@0.18.20": version "0.18.20" @@ -3046,10 +3046,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.0.tgz#ac617e0dc14e9758d3d7efd70288c14122557dc7" integrity sha512-H9Eu6MGse++204XZcYsse1yFHmRXEWgadk2N58O/xd50P9EvFMLJTQLg+lB4E1cF2xhLZU5luSWtGTb0l9UeSg== -"@esbuild/netbsd-x64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz#7d4f4041e30c5c07dd24ffa295c73f06038ec775" - integrity sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA== +"@esbuild/linux-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff" + integrity sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw== "@esbuild/netbsd-x64@0.18.20": version "0.18.20" @@ -3071,10 +3071,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.0.tgz#6cc778567f1513da6e08060e0aeb41f82eb0f53c" integrity sha512-lCT675rTN1v8Fo+RGrE5KjSnfY0x9Og4RN7t7lVrN3vMSjy34/+3na0q7RIfWDAj0e0rCh0OL+P88lu3Rt21MQ== -"@esbuild/openbsd-x64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz#970fa7f8470681f3e6b1db0cc421a4af8060ec35" - integrity sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg== +"@esbuild/netbsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6" + integrity sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ== "@esbuild/openbsd-x64@0.18.20": version "0.18.20" @@ -3096,10 +3096,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.0.tgz#76848bcf76b4372574fb4d06cd0ed1fb29ec0fbe" integrity sha512-HKoUGXz/TOVXKQ+67NhxyHv+aDSZf44QpWLa3I1lLvAwGq8x1k0T+e2HHSRvxWhfJrFxaaqre1+YyzQ99KixoA== -"@esbuild/sunos-x64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz#abc60e7c4abf8b89fb7a4fe69a1484132238022c" - integrity sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw== +"@esbuild/openbsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf" + integrity sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ== "@esbuild/sunos-x64@0.18.20": version "0.18.20" @@ -3121,10 +3121,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.0.tgz#ea4cd0639bf294ad51bc08ffbb2dac297e9b4706" integrity sha512-GDwAqgHQm1mVoPppGsoq4WJwT3vhnz/2N62CzhvApFD1eJyTroob30FPpOZabN+FgCjhG+AgcZyOPIkR8dfD7g== -"@esbuild/win32-arm64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz#7b0ff9e8c3265537a7a7b1fd9a24e7bd39fcd87a" - integrity sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw== +"@esbuild/sunos-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f" + integrity sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w== "@esbuild/win32-arm64@0.18.20": version "0.18.20" @@ -3146,10 +3146,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.0.tgz#a5c171e4a7f7e4e8be0e9947a65812c1535a7cf0" integrity sha512-0vYsP8aC4TvMlOQYozoksiaxjlvUcQrac+muDqj1Fxy6jh9l9CZJzj7zmh8JGfiV49cYLTorFLxg7593pGldwQ== -"@esbuild/win32-ia32@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz#e90fe5267d71a7b7567afdc403dfd198c292eb09" - integrity sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig== +"@esbuild/win32-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90" + integrity sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ== "@esbuild/win32-ia32@0.18.20": version "0.18.20" @@ -3171,10 +3171,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.0.tgz#f8ac5650c412d33ea62d7551e0caf82da52b7f85" integrity sha512-p98u4rIgfh4gdpV00IqknBD5pC84LCub+4a3MO+zjqvU5MVXOc3hqR2UgT2jI2nh3h8s9EQxmOsVI3tyzv1iFg== -"@esbuild/win32-x64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz#c5a1a4bfe1b57f0c3e61b29883525c6da3e5c091" - integrity sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q== +"@esbuild/win32-ia32@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23" + integrity sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ== "@esbuild/win32-x64@0.18.20": version "0.18.20" @@ -3196,6 +3196,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.0.tgz#2efddf82828aac85e64cef62482af61c29561bee" integrity sha512-NgJnesu1RtWihtTtXGFMU5YSE6JyyHPMxCwBZK7a6/8d31GuSo9l0Ss7w1Jw5QnKUawG6UEehs883kcXf5fYwg== +"@esbuild/win32-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc" + integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ== + "@eslint-community/eslint-utils@^4.1.2", "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -4278,10 +4283,10 @@ resolved "https://registry.yarnpkg.com/@lukeed/csprng/-/csprng-1.1.0.tgz#1e3e4bd05c1cc7a0b2ddbd8a03f39f6e4b5e6cfe" integrity sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA== -"@nestjs/common@^10.3.3": - version "10.3.3" - resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-10.3.3.tgz#ba20f756dbed62f5fe29737c42384ad41156c9e9" - integrity sha512-LAkTe8/CF0uNWM0ecuDwUNTHCi1lVSITmmR4FQ6Ftz1E7ujQCnJ5pMRzd8JRN14vdBkxZZ8VbVF0BDUKoKNxMQ== +"@nestjs/common@^10.3.7": + version "10.3.7" + resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-10.3.7.tgz#38ab5ff92277cf1f26f4749c264524e76962cfff" + integrity sha512-gKFtFzcJznrwsRYjtNZoPAvSOPYdNgxbTYoAyLTpoy393cIKgLmJTHu6ReH8/qIB9AaZLdGaFLkx98W/tFWFUw== dependencies: uid "2.0.2" iterare "1.2.1" @@ -5632,11 +5637,23 @@ dependencies: "@sentry-internal/rrweb-snapshot" "2.11.0" +"@sentry-internal/rrdom@2.12.0": + version "2.12.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.12.0.tgz#d3ca32b1e4b8c5d8cc9bdb44f933fe4b059573a0" + integrity sha512-EQ9vmhkTREdtzKp6SmD4GEkwr+RJcaEnbVcDZjbnQnxagskOpqvXjoPMONPf9hZhkULwnrnyFGGp0VpQOGBS0w== + dependencies: + "@sentry-internal/rrweb-snapshot" "2.12.0" + "@sentry-internal/rrweb-snapshot@2.11.0": version "2.11.0" resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.11.0.tgz#1af79130604afea989d325465b209ac015b27c9a" integrity sha512-1nP22QlplMNooSNvTh+L30NSZ+E3UcfaJyxXSMLxUjQHTGPyM1VkndxZMmxlKhyR5X+rLbxi/+RvuAcpM43VoA== +"@sentry-internal/rrweb-snapshot@2.12.0": + version "2.12.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.12.0.tgz#2f1f6d4867a07ab757475fb4fa337d7f1aaa6b2d" + integrity sha512-AYo8CeDA7qDOKFG75E+bnxrS/qm7l5Ad0ftClA3VzoGV58bNNgv/aKiECtUPk0UPs4EqTQ8z8W/MZ9EYDF6vvA== + "@sentry-internal/rrweb-types@2.11.0": version "2.11.0" resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.11.0.tgz#e598c133b87be1fb04d31d09773b86142b095072" @@ -5644,6 +5661,13 @@ dependencies: "@sentry-internal/rrweb-snapshot" "2.11.0" +"@sentry-internal/rrweb-types@2.12.0": + version "2.12.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.12.0.tgz#f7c57eda7610882c71860437657ffbbcb788184d" + integrity sha512-W0iLlTx3HeapBTGjg/uLoKQr1/DGPbkANqwjf4mW0IS4jHAVcxFX/e769aHHKEmd68Lm3+A8b08xdA9UDBXW5w== + dependencies: + "@sentry-internal/rrweb-snapshot" "2.12.0" + "@sentry-internal/rrweb@2.11.0": version "2.11.0" resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.11.0.tgz#be8e8dfff2acf64d418b625d35a20fdcd7daeb96" @@ -5658,6 +5682,20 @@ fflate "^0.4.4" mitt "^3.0.0" +"@sentry-internal/rrweb@2.12.0": + version "2.12.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.12.0.tgz#4becbedf7315f4b4e0ebc35319a848ec6f082dce" + integrity sha512-NosAF5f8dXdj6linXpI+e38/eKVtwy3R2rzmMohBCwdhPXgTkTV/Laj/9OsRxARNRyz81mIEGcn/Ivp/De7RaA== + dependencies: + "@sentry-internal/rrdom" "2.12.0" + "@sentry-internal/rrweb-snapshot" "2.12.0" + "@sentry-internal/rrweb-types" "2.12.0" + "@types/css-font-loading-module" "0.0.7" + "@xstate/fsm" "^1.4.0" + base64-arraybuffer "^1.0.1" + fflate "^0.4.4" + mitt "^3.0.0" + "@sentry/babel-plugin-component-annotate@2.14.2": version "2.14.2" resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.14.2.tgz#d756bed93495e97a5a2aad56e2a6dc5020305adc" @@ -5696,45 +5734,45 @@ magic-string "0.27.0" unplugin "1.0.1" -"@sentry/cli-darwin@2.30.2": - version "2.30.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.30.2.tgz#a592227f428119c1239d76426ee76f895d89d521" - integrity sha512-lZkKXMt0HUAwLQuPpi/DM3CsdCCp+6B2cdur+8fAq7uARXTOsTKVDxv9pkuJHCgHUnguh8ittP5GMr0baTxmMg== - -"@sentry/cli-linux-arm64@2.30.2": - version "2.30.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.30.2.tgz#b5d2314e27d0bb75f5a375282e77d2cd74d0690b" - integrity sha512-IWassuXggNhHOPCNrORNmd5SrAx5rU4XDlgOWBJr/ez7DvlPrr9EhV1xsdht6K4mPXhCGJq3rtRdCoWGJQW6Uw== - -"@sentry/cli-linux-arm@2.30.2": - version "2.30.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.30.2.tgz#7f1ef0e7b50734e176290e99c6237fd99425d6e3" - integrity sha512-H7hqiLpEL7w/EHdhuUGatwg9O080mdujq4/zS96buKIHXxZE6KqMXGtMVIAvTl1+z6BlBEnfvZGI19MPw3t/7w== - -"@sentry/cli-linux-i686@2.30.2": - version "2.30.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.30.2.tgz#e15182f8afb203095bb49bd621adcc91b3b785d3" - integrity sha512-gZIq131M4TJTG1lX9uvpoaGWaEXCEfdDXrXu/z/YZmAKBcThpMYChodXmm8FB6X4xb0TPXzIFqdzlLdglFK46g== - -"@sentry/cli-linux-x64@2.30.2": - version "2.30.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.30.2.tgz#dce04b823f0fc54999565da32439c01349872568" - integrity sha512-NmTAIl7aW9OHxwB4149sBfvCbTyK9T/CvBX38keaD2yIThet9gZ4koP49hBDxYF99aQX3E+LIAqWwnkV9W72Sw== - -"@sentry/cli-win32-i686@2.30.2": - version "2.30.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.30.2.tgz#54a8ee04b59d6004555f6d833ca17dc8c3e27402" - integrity sha512-SBR/Q3T6o+7uHwHNdjcG9GA3R++9w8oi778b95GuOC3dh0WOU6hXaKwQWe95ZcuSd2rKpouH7dhMjqqNM4HxOA== - -"@sentry/cli-win32-x64@2.30.2": - version "2.30.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.30.2.tgz#1e84df37e9f0e5743b42435f92982cf7dae5e6d8" - integrity sha512-gF9wSZxzXFgakkC+uKVLAAYlbYj13e1gTsNm3gm+ODfpV+rbHwvbKoLfNsbVCFVCEZxIV2rXEP5WmTr0kiMvWQ== - -"@sentry/cli@^2.22.3", "@sentry/cli@^2.30.2": - version "2.30.2" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.30.2.tgz#5f62ec56685808875577792dfdc7de1d047905a8" - integrity sha512-jQ/RBJ3bZ4PFbfOsGq8EykygHHmXXPw+i6jqsnQfAPIeZoX+DsqpAZbYubQEZKekmQ8EVGFxGHzUVkd6hLVMbA== +"@sentry/cli-darwin@2.31.0": + version "2.31.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.31.0.tgz#59e0805db8926a55676c74690e5083a0a78ae11f" + integrity sha512-VM5liyxMnm4K2g0WsrRPXRCMLhaT09C7gK5Fz/CxKYh9sbMZB7KA4hV/3klkyuyw1+ECF1J66cefhNkFZepUig== + +"@sentry/cli-linux-arm64@2.31.0": + version "2.31.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.31.0.tgz#38604d2d1e7c2e50d48610d38523e371d2104cd7" + integrity sha512-eENJTmXoFX3uNr8xRW7Bua2Sw3V1tylQfdtS85pNjZPdbm3U8wYQSWu2VoZkK2ASOoC+17YC8jTQxq62KWnSeQ== + +"@sentry/cli-linux-arm@2.31.0": + version "2.31.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.31.0.tgz#6e802a279011703d39e4b31de7b950c522a73261" + integrity sha512-AZoCN3waXEfXGCd3YSrikcX/y63oQe0Tiyapkeoifq/0QhI+2MOOrAQb60gthsXwb0UDK/XeFi3PaxyUCphzxA== + +"@sentry/cli-linux-i686@2.31.0": + version "2.31.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.31.0.tgz#d4586a18145f43b37324231e0f19f8f23793fc58" + integrity sha512-cQUFb3brhLaNSIoNzjU/YASnTM1I3TDJP9XXzH0eLK9sSopCcDcc6OrYEYvdjJXZKzFv5sbc9UNMsIDbh4+rYg== + +"@sentry/cli-linux-x64@2.31.0": + version "2.31.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.31.0.tgz#f89fd87b47a5eb10c292846f3a1a754cf97105fe" + integrity sha512-z1zTNg91nZJRdcGHC/bCU1KwIaifV0MLJteip9KrFDprzhJk1HtMxFOS0+OZ5/UH21CjAFmg9Pj6IAGqm3BYjA== + +"@sentry/cli-win32-i686@2.31.0": + version "2.31.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.31.0.tgz#cb3dbb539c8f8bcac4b1f95ab45a87b5143997ee" + integrity sha512-+K7fdk57aUd4CmYrQfDGYPzVyxsTnVro6IPb5QSSLpP03dL7ko5208epu4m2SyN/MkFvscy9Di3n3DTvIfDU2w== + +"@sentry/cli-win32-x64@2.31.0": + version "2.31.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.31.0.tgz#8ac3fa4ae0634911af4f4a497d58d2adce0f303a" + integrity sha512-w5cvpZ6VVlhlyleY8TYHmrP7g48vKHnoVt5xFccfxT+HqQI/AxodvzgVvBTM2kB/sh/kHwexp6bJGWCdkGftww== + +"@sentry/cli@^2.22.3", "@sentry/cli@^2.31.0": + version "2.31.0" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.31.0.tgz#a659216576fef56733de659057d6b9039d0b64e9" + integrity sha512-nCESoXAG3kRUO5n3QbDYAqX6RU3z1ORjnd7a3sqijYsCGHfOpcjGdS7JYLVg5if+tXMEF5529BPXFe5Kg/J9tw== dependencies: https-proxy-agent "^5.0.0" node-fetch "^2.6.7" @@ -5742,13 +5780,13 @@ proxy-from-env "^1.1.0" which "^2.0.2" optionalDependencies: - "@sentry/cli-darwin" "2.30.2" - "@sentry/cli-linux-arm" "2.30.2" - "@sentry/cli-linux-arm64" "2.30.2" - "@sentry/cli-linux-i686" "2.30.2" - "@sentry/cli-linux-x64" "2.30.2" - "@sentry/cli-win32-i686" "2.30.2" - "@sentry/cli-win32-x64" "2.30.2" + "@sentry/cli-darwin" "2.31.0" + "@sentry/cli-linux-arm" "2.31.0" + "@sentry/cli-linux-arm64" "2.31.0" + "@sentry/cli-linux-i686" "2.31.0" + "@sentry/cli-linux-x64" "2.31.0" + "@sentry/cli-win32-i686" "2.31.0" + "@sentry/cli-win32-x64" "2.31.0" "@sentry/vite-plugin@2.14.2", "@sentry/vite-plugin@^2.14.2": version "2.14.2" @@ -6161,9 +6199,9 @@ "@types/chai" "*" "@types/chai-subset@^1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@types/chai-subset/-/chai-subset-1.3.3.tgz#97893814e92abd2c534de422cb377e0e0bdaac94" - integrity sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw== + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/chai-subset/-/chai-subset-1.3.5.tgz#3fc044451f26985f45625230a7f22284808b0a9a" + integrity sha512-c2mPnw+xHtXDoHmdtcCXGwyLMiauiAyxWMzhGpqHC4nqI/Y5G2XhTampslK2rb59kpcuHon03UH8W6iYUzw88A== dependencies: "@types/chai" "*" @@ -6173,9 +6211,9 @@ integrity sha512-rYff6FI+ZTKAPkJUoyz7Udq3GaoDZnxYDEvdEdFZASiA7PoErltHezDishqQiSDWrGxvxmplH304jyzQmjp0AQ== "@types/chai@^4.3.4": - version "4.3.4" - resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.4.tgz#e913e8175db8307d78b4e8fa690408ba6b65dee4" - integrity sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw== + version "4.3.14" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.14.tgz#ae3055ea2be43c91c9fd700a36d67820026d96e6" + integrity sha512-Wj71sXE4Q4AkGdG9Tvq1u/fquNz9EdG4LIJMwVVII7ashjD/8cf8fyIfJAjRr6YcsXnSE8cOGQPq1gqeR8z+3w== "@types/connect-history-api-fallback@^1.3.5": version "1.5.4" @@ -6197,11 +6235,6 @@ resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.8.tgz#6742a5971f490dc41e59d277eee71361fea0b537" integrity sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg== -"@types/cookie@0.5.2": - version "0.5.2" - resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.5.2.tgz#9bf9d62c838c85a07c92fdf2334c2c14fd9c59a9" - integrity sha512-DBpRoJGKJZn7RY92dPrgoMew8xCWc2P71beqsjyhEI/Ds9mOyVmBwtekyfhpwFIVt1WrxTonFifiOZ62V8CnNA== - "@types/cookie@^0.4.0", "@types/cookie@^0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" @@ -6755,11 +6788,6 @@ resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== -"@types/lru-cache@^5.1.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.0.tgz#57f228f2b80c046b4a1bd5cac031f81f207f4f03" - integrity sha512-RaE0B+14ToE4l6UqdarKPnXwVDuigfFv+5j9Dze/Nqr23yyuqdNvzcZi3xB+3Agvi5R4EOgAksfv3lXX4vBt9w== - "@types/luxon@~3.3.0": version "3.3.8" resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.3.8.tgz#84dbf2d020a9209a272058725e168f21d331a67e" @@ -6870,11 +6898,6 @@ 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.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== - "@types/node@16.18.70": version "16.18.70" resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.70.tgz#d4c819be1e9f8b69a794d6f2fd929d9ff76f6d4b" @@ -6890,6 +6913,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b" integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== +"@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== + "@types/node@^18.11.17": version "18.14.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.14.2.tgz#c076ed1d7b6095078ad3cf21dfeea951842778b1" @@ -7463,21 +7491,21 @@ integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== "@vitest/coverage-c8@^0.29.2": - version "0.29.2" - resolved "https://registry.yarnpkg.com/@vitest/coverage-c8/-/coverage-c8-0.29.2.tgz#30b81e32ff11c20e2f3ab78c84e21b4c6c08190c" - integrity sha512-NmD3WirQCeQjjKfHu4iEq18DVOBFbLn9TKVdMpyi5YW2EtnS+K22/WE+9/wRrepOhyeTxuEFgxUVkCAE1GhbnQ== + version "0.29.8" + resolved "https://registry.yarnpkg.com/@vitest/coverage-c8/-/coverage-c8-0.29.8.tgz#a61f5a2434af3f0fd7a7e4fe9c25bb6ed03ebd0a" + integrity sha512-y+sEMQMctWokjnSqm3FCQEYFkjLrYaznsxEZHxcx8z2aftpYg3A5tvI1S5himfdEFo7o+OeHzh40bPSWZHW4oQ== dependencies: c8 "^7.13.0" picocolors "^1.0.0" std-env "^3.3.1" -"@vitest/expect@0.29.2": - version "0.29.2" - resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-0.29.2.tgz#7503aabd72764612b0bc8258bafa3232ccb81586" - integrity sha512-wjrdHB2ANTch3XKRhjWZN0UueFocH0cQbi2tR5Jtq60Nb3YOSmakjdAvUa2JFBu/o8Vjhj5cYbcMXkZxn1NzmA== +"@vitest/expect@0.29.8": + version "0.29.8" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-0.29.8.tgz#6ecdd031b4ea8414717d10b65ccd800908384612" + integrity sha512-xlcVXn5I5oTq6NiZSY3ykyWixBxr5mG8HYtjvpgg6KaqHm0mvhX18xuwl5YGxIRNt/A5jidd7CWcNHrSvgaQqQ== dependencies: - "@vitest/spy" "0.29.2" - "@vitest/utils" "0.29.2" + "@vitest/spy" "0.29.8" + "@vitest/utils" "0.29.8" chai "^4.3.7" "@vitest/expect@1.4.0": @@ -7489,12 +7517,12 @@ "@vitest/utils" "1.4.0" chai "^4.3.10" -"@vitest/runner@0.29.2": - version "0.29.2" - resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-0.29.2.tgz#bbc7b239758de4158392bb343e48ee5a4aa507e1" - integrity sha512-A1P65f5+6ru36AyHWORhuQBJrOOcmDuhzl5RsaMNFe2jEkoj0faEszQS4CtPU/LxUYVIazlUtZTY0OEZmyZBnA== +"@vitest/runner@0.29.8": + version "0.29.8" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-0.29.8.tgz#ede8a7be8a074ea1180bc1d1595bd879ed15971c" + integrity sha512-FzdhnRDwEr/A3Oo1jtIk/B952BBvP32n1ObMEb23oEJNO+qO5cBet6M2XWIDQmA7BDKGKvmhUf2naXyp/2JEwQ== dependencies: - "@vitest/utils" "0.29.2" + "@vitest/utils" "0.29.8" p-limit "^4.0.0" pathe "^1.1.0" @@ -7516,10 +7544,10 @@ pathe "^1.1.1" pretty-format "^29.7.0" -"@vitest/spy@0.29.2": - version "0.29.2" - resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-0.29.2.tgz#4210d844fabd9a68a1d2932d6a26c051bd089021" - integrity sha512-Hc44ft5kaAytlGL2PyFwdAsufjbdOvHklwjNy/gy/saRbg9Kfkxfh+PklLm1H2Ib/p586RkQeNFKYuJInUssyw== +"@vitest/spy@0.29.8": + version "0.29.8" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-0.29.8.tgz#2e0c3b30e04d317b2197e3356234448aa432e131" + integrity sha512-VdjBe9w34vOMl5I5mYEzNX8inTxrZ+tYUVk9jxaZJmHFwmDFC/GV3KBFTA/JKswr3XHvZL+FE/yq5EVhb6pSAw== dependencies: tinyspy "^1.0.2" @@ -7530,15 +7558,14 @@ dependencies: tinyspy "^2.2.0" -"@vitest/utils@0.29.2": - version "0.29.2" - resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-0.29.2.tgz#8990794a6855de19b59da80413dc5a1e1991da4d" - integrity sha512-F14/Uc+vCdclStS2KEoXJlOLAEyqRhnw0gM27iXw9bMTcyKRPJrQ+rlC6XZ125GIPvvKYMPpVxNhiou6PsEeYQ== +"@vitest/utils@0.29.8": + version "0.29.8" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-0.29.8.tgz#423da85fd0c6633f3ab496cf7d2fc0119b850df8" + integrity sha512-qGzuf3vrTbnoY+RjjVVIBYfuWMjn3UMUqyQtdGNZ6ZIIyte7B37exj6LaVkrZiUTvzSadVvO/tJm8AEgbGCBPg== dependencies: cli-truncate "^3.1.0" diff "^5.1.0" loupe "^2.3.6" - picocolors "^1.0.0" pretty-format "^27.5.1" "@vitest/utils@1.4.0": @@ -7662,6 +7689,14 @@ "@webassemblyjs/helper-numbers" "1.11.6" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" +"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" + integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ast@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" @@ -7711,6 +7746,11 @@ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093" integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA== +"@webassemblyjs/helper-buffer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6" + integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw== + "@webassemblyjs/helper-buffer@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz#a1442d269c5feb23fcbc9ef759dac3547f29de00" @@ -7788,6 +7828,16 @@ "@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/wasm-gen" "1.11.6" +"@webassemblyjs/helper-wasm-section@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf" + integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/helper-wasm-section@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz#5a4138d5a6292ba18b04c5ae49717e4167965346" @@ -7897,6 +7947,20 @@ "@webassemblyjs/wasm-parser" "1.11.6" "@webassemblyjs/wast-printer" "1.11.6" +"@webassemblyjs/wasm-edit@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" + integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-opt" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + "@webassemblyjs/wast-printer" "1.12.1" + "@webassemblyjs/wasm-gen@1.11.1": version "1.11.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76" @@ -7919,6 +7983,17 @@ "@webassemblyjs/leb128" "1.11.6" "@webassemblyjs/utf8" "1.11.6" +"@webassemblyjs/wasm-gen@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547" + integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + "@webassemblyjs/wasm-gen@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz#50bc70ec68ded8e2763b01a1418bf43491a7a49c" @@ -7950,6 +8025,16 @@ "@webassemblyjs/wasm-gen" "1.11.6" "@webassemblyjs/wasm-parser" "1.11.6" +"@webassemblyjs/wasm-opt@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5" + integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + "@webassemblyjs/wasm-opt@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz#2211181e5b31326443cc8112eb9f0b9028721a61" @@ -7984,6 +8069,18 @@ "@webassemblyjs/leb128" "1.11.6" "@webassemblyjs/utf8" "1.11.6" +"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" + integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-api-error" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + "@webassemblyjs/wasm-parser@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz#9d48e44826df4a6598294aa6c87469d642fff65e" @@ -8024,6 +8121,14 @@ "@webassemblyjs/ast" "1.11.6" "@xtuc/long" "4.2.2" +"@webassemblyjs/wast-printer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac" + integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@xtuc/long" "4.2.2" + "@webassemblyjs/wast-printer@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz#4935d54c85fef637b00ce9f52377451d00d47899" @@ -8154,16 +8259,16 @@ acorn-walk@^7.1.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== -acorn-walk@^8.1.1, acorn-walk@^8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" - integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== - -acorn-walk@^8.3.2: +acorn-walk@^8.0.0, acorn-walk@^8.2.0, acorn-walk@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.2.tgz#7703af9415f1b6db9315d6895503862e231d34aa" integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A== +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + acorn@8.8.2, acorn@^8.8.1, acorn@^8.8.2: version "8.8.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" @@ -8179,16 +8284,16 @@ acorn@^7.1.1, acorn@^7.4.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== +acorn@^8.0.4, acorn@^8.11.3: + version "8.11.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== + acorn@^8.10.0: version "8.10.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== -acorn@^8.11.3: - version "8.11.3" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" - integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== - acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.0, acorn@^8.7.1: version "8.8.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" @@ -10893,9 +10998,9 @@ bytes@3.1.2: integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== c8@^7.13.0: - version "7.13.0" - resolved "https://registry.yarnpkg.com/c8/-/c8-7.13.0.tgz#a2a70a851278709df5a9247d62d7f3d4bcb5f2e4" - integrity sha512-/NL4hQTv1gBL6J6ei80zu3IiTrmePDKXKXOTLpHvcIWZTVYQlDhVWjjWvkhICylE8EwwnMVzDZugCvdx0/DIIA== + version "7.14.0" + resolved "https://registry.yarnpkg.com/c8/-/c8-7.14.0.tgz#f368184c73b125a80565e9ab2396ff0be4d732f3" + integrity sha512-i04rtkkcNcCf7zsQcSv/T9EbUn4RXQ6mropeMcjFOsQXQ0iGLAr/xT6TImQg4+U9hmNpN9XdvPkjUL1IzbgxJw== dependencies: "@bcoe/v8-coverage" "^0.2.3" "@istanbuljs/schema" "^0.1.3" @@ -11177,7 +11282,7 @@ chai@^4.1.2: pathval "^1.1.1" type-detect "^4.0.5" -chai@^4.3.10: +chai@^4.3.10, chai@^4.3.7: version "4.4.1" resolved "https://registry.yarnpkg.com/chai/-/chai-4.4.1.tgz#3603fa6eba35425b0f2ac91a009fe924106e50d1" integrity sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g== @@ -11190,19 +11295,6 @@ chai@^4.3.10: pathval "^1.1.1" type-detect "^4.0.8" -chai@^4.3.7: - version "4.3.7" - resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.7.tgz#ec63f6df01829088e8bf55fca839bcd464a8ec51" - integrity sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A== - dependencies: - assertion-error "^1.1.0" - check-error "^1.0.2" - deep-eql "^4.1.2" - get-func-name "^2.0.0" - loupe "^2.3.1" - pathval "^1.1.1" - type-detect "^4.0.5" - chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -11768,7 +11860,7 @@ commander@2.8.x: dependencies: graceful-readlink ">= 1.0.0" -commander@7.2.0: +commander@7.2.0, commander@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== @@ -12542,7 +12634,7 @@ dateformat@^3.0.3: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== -debounce@^1.2.0: +debounce@^1.2.0, debounce@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== @@ -12645,7 +12737,7 @@ deep-eql@^3.0.1: dependencies: type-detect "^4.0.0" -deep-eql@^4.1.2, deep-eql@^4.1.3: +deep-eql@^4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.3.tgz#7c7775513092f7df98d8df9996dd085eb668cc6d" integrity sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw== @@ -13155,7 +13247,7 @@ duplexer3@^0.1.4: resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= -duplexer@^0.1.1, duplexer@~0.1.1: +duplexer@^0.1.1, duplexer@^0.1.2, duplexer@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== @@ -13992,7 +14084,7 @@ enhanced-resolve@^4.5.0: memory-fs "^0.5.0" tapable "^1.0.0" -enhanced-resolve@^5.10.0: +enhanced-resolve@^5.10.0, enhanced-resolve@^5.16.0: version "5.16.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz#65ec88778083056cb32487faa9aef82ed0864787" integrity sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA== @@ -14482,34 +14574,6 @@ esbuild@^0.15.0: esbuild-windows-64 "0.15.18" esbuild-windows-arm64 "0.15.18" -esbuild@^0.16.14, esbuild@^0.16.3: - version "0.16.17" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.16.17.tgz#fc2c3914c57ee750635fee71b89f615f25065259" - integrity sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg== - optionalDependencies: - "@esbuild/android-arm" "0.16.17" - "@esbuild/android-arm64" "0.16.17" - "@esbuild/android-x64" "0.16.17" - "@esbuild/darwin-arm64" "0.16.17" - "@esbuild/darwin-x64" "0.16.17" - "@esbuild/freebsd-arm64" "0.16.17" - "@esbuild/freebsd-x64" "0.16.17" - "@esbuild/linux-arm" "0.16.17" - "@esbuild/linux-arm64" "0.16.17" - "@esbuild/linux-ia32" "0.16.17" - "@esbuild/linux-loong64" "0.16.17" - "@esbuild/linux-mips64el" "0.16.17" - "@esbuild/linux-ppc64" "0.16.17" - "@esbuild/linux-riscv64" "0.16.17" - "@esbuild/linux-s390x" "0.16.17" - "@esbuild/linux-x64" "0.16.17" - "@esbuild/netbsd-x64" "0.16.17" - "@esbuild/openbsd-x64" "0.16.17" - "@esbuild/sunos-x64" "0.16.17" - "@esbuild/win32-arm64" "0.16.17" - "@esbuild/win32-ia32" "0.16.17" - "@esbuild/win32-x64" "0.16.17" - esbuild@^0.18.10: version "0.18.20" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.18.20.tgz#4709f5a34801b43b799ab7d6d82f7284a9b7a7a6" @@ -14594,6 +14658,35 @@ esbuild@^0.19.3: "@esbuild/win32-ia32" "0.19.9" "@esbuild/win32-x64" "0.19.9" +esbuild@^0.20.1: + version "0.20.2" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.2.tgz#9d6b2386561766ee6b5a55196c6d766d28c87ea1" + integrity sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g== + optionalDependencies: + "@esbuild/aix-ppc64" "0.20.2" + "@esbuild/android-arm" "0.20.2" + "@esbuild/android-arm64" "0.20.2" + "@esbuild/android-x64" "0.20.2" + "@esbuild/darwin-arm64" "0.20.2" + "@esbuild/darwin-x64" "0.20.2" + "@esbuild/freebsd-arm64" "0.20.2" + "@esbuild/freebsd-x64" "0.20.2" + "@esbuild/linux-arm" "0.20.2" + "@esbuild/linux-arm64" "0.20.2" + "@esbuild/linux-ia32" "0.20.2" + "@esbuild/linux-loong64" "0.20.2" + "@esbuild/linux-mips64el" "0.20.2" + "@esbuild/linux-ppc64" "0.20.2" + "@esbuild/linux-riscv64" "0.20.2" + "@esbuild/linux-s390x" "0.20.2" + "@esbuild/linux-x64" "0.20.2" + "@esbuild/netbsd-x64" "0.20.2" + "@esbuild/openbsd-x64" "0.20.2" + "@esbuild/sunos-x64" "0.20.2" + "@esbuild/win32-arm64" "0.20.2" + "@esbuild/win32-ia32" "0.20.2" + "@esbuild/win32-x64" "0.20.2" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -16692,7 +16785,7 @@ got@^9.6.0: to-readable-stream "^1.0.0" url-parse-lax "^3.0.0" -graceful-fs@4.2.11, graceful-fs@^4.1.5: +graceful-fs@4.2.11, graceful-fs@^4.1.5, graceful-fs@^4.2.11: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -16760,6 +16853,13 @@ gud@^1.0.0: resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0" integrity sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw== +gzip-size@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462" + integrity sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q== + dependencies: + duplexer "^0.1.2" + handle-thing@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" @@ -17305,7 +17405,7 @@ html-entities@^2.3.2: resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.5.2.tgz#201a3cf95d3a15be7099521620d19dfb4f65359f" integrity sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA== -html-escaper@^2.0.0: +html-escaper@^2.0.0, html-escaper@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== @@ -17458,7 +17558,7 @@ https-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= -https-proxy-agent@5.0.1: +https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== @@ -17482,14 +17582,6 @@ https-proxy-agent@^4.0.0: agent-base "5" debug "4" -https-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" - integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== - dependencies: - agent-base "6" - debug "4" - https@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/https/-/https-1.0.0.tgz#3c37c7ae1a8eeb966904a2ad1e975a194b7ed3a4" @@ -17825,6 +17917,27 @@ inquirer@^7.0.1: strip-ansi "^6.0.0" through "^2.3.6" +inquirer@^8.2.0: + version "8.2.6" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.6.tgz#733b74888195d8d400a67ac332011b5fae5ea562" + integrity sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.1" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.21" + mute-stream "0.0.8" + ora "^5.4.1" + run-async "^2.4.0" + rxjs "^7.5.5" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + wrap-ansi "^6.0.1" + inquirer@^8.2.4: version "8.2.5" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.5.tgz#d8654a7542c35a9b9e069d27e2df4858784d54f8" @@ -18525,9 +18638,9 @@ istanbul-reports@^3.1.3: istanbul-lib-report "^3.0.0" istanbul-reports@^3.1.4: - version "3.1.5" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.5.tgz#cc9a6ab25cb25659810e4785ed9d9fb742578bae" - integrity sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w== + version "3.1.7" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.7.tgz#daed12b9e1dca518e15c056e1e537e741280fa0b" + integrity sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g== dependencies: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" @@ -20382,7 +20495,7 @@ loud-rejection@^1.0.0: currently-unhandled "^0.4.1" signal-exit "^3.0.0" -loupe@^2.3.1, loupe@^2.3.6: +loupe@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.6.tgz#76e4af498103c532d1ecc9be102036a21f787b53" integrity sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA== @@ -21696,17 +21809,7 @@ mktemp@~0.4.0: resolved "https://registry.yarnpkg.com/mktemp/-/mktemp-0.4.0.tgz#6d0515611c8a8c84e484aa2000129b98e981ff0b" integrity sha1-bQUVYRyKjITkhKogABKbmOmB/ws= -mlly@^1.1.0, mlly@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.2.0.tgz#f0f6c2fc8d2d12ea6907cd869066689b5031b613" - integrity sha512-+c7A3CV0KGdKcylsI6khWyts/CYrGTrRVo4R/I7u/cUsy0Conxa6LUhiEzVKIw14lc2L5aiO4+SeVe4TeGRKww== - dependencies: - acorn "^8.8.2" - pathe "^1.1.0" - pkg-types "^1.0.2" - ufo "^1.1.1" - -mlly@^1.2.0, mlly@^1.4.2: +mlly@^1.1.0, mlly@^1.2.0, mlly@^1.4.2: version "1.6.1" resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.6.1.tgz#0983067dc3366d6314fc5e12712884e6978d028f" integrity sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA== @@ -21716,7 +21819,7 @@ mlly@^1.2.0, mlly@^1.4.2: pkg-types "^1.0.3" ufo "^1.3.2" -mocha@^6.1.4, mocha@^6.2.0: +mocha@^6.1.4: version "6.2.3" resolved "https://registry.yarnpkg.com/mocha/-/mocha-6.2.3.tgz#e648432181d8b99393410212664450a4c1e31912" integrity sha512-0R/3FvjIGH3eEuG17ccFPk117XL2rWxatr81a57D+r/x2uTYZRbdZ4oVidEUMh2W2TJDa7MdAb12Lm2/qrKajg== @@ -22240,7 +22343,7 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" -nock@^13.0.4, nock@^13.0.5, nock@^13.1.0: +nock@^13.0.4, nock@^13.1.0: version "13.2.4" resolved "https://registry.yarnpkg.com/nock/-/nock-13.2.4.tgz#43a309d93143ee5cdcca91358614e7bde56d20e1" integrity sha512-8GPznwxcPNCH/h8B+XZcKjYPXnUV5clOKCjAqyjsiqA++MpNx9E9+t8YPp0MbThO+KauRo7aZJ1WuIZmOrT2Ug== @@ -23070,6 +23173,11 @@ open@^8.0.9: is-docker "^2.1.1" is-wsl "^2.2.0" +opener@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" + integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== + 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" @@ -23987,15 +24095,6 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -pkg-types@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.0.2.tgz#c233efc5210a781e160e0cafd60c0d0510a4b12e" - integrity sha512-hM58GKXOcj8WTqUXnsQyJYXdeAPbythQgEF3nTcEo+nkD49chjQ9IKm/QJy9xf6JakXptz86h7ecP2024rrLaQ== - dependencies: - jsonc-parser "^3.2.0" - mlly "^1.1.1" - pathe "^1.1.0" - pkg-types@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.0.3.tgz#988b42ab19254c01614d13f4f65a2cfc7880f868" @@ -24472,7 +24571,7 @@ postcss@8.4.31, postcss@^8.4.27: picocolors "^1.0.0" source-map-js "^1.0.2" -postcss@^8.1.10, postcss@^8.1.7, postcss@^8.2.15, postcss@^8.3.7, postcss@^8.4.20, postcss@^8.4.21: +postcss@^8.1.10, postcss@^8.1.7, postcss@^8.2.15, postcss@^8.3.7: version "8.4.24" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.24.tgz#f714dba9b2284be3cc07dbd2fc57ee4dc972d2df" integrity sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg== @@ -24481,7 +24580,7 @@ postcss@^8.1.10, postcss@^8.1.7, postcss@^8.2.15, postcss@^8.3.7, postcss@^8.4.2 picocolors "^1.0.0" source-map-js "^1.0.2" -postcss@^8.2.14, postcss@^8.4.35, postcss@^8.4.7, postcss@^8.4.8: +postcss@^8.2.14, postcss@^8.4.7, postcss@^8.4.8: version "8.4.36" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.36.tgz#dba513c3c3733c44e0288a712894f8910bbaabc6" integrity sha512-/n7eumA6ZjFHAsbX30yhHup/IMkOmlmvtEi7P+6RMYf+bGJSUHc3geH4a0NSZxAz/RJfiS9tooCTs9LAVYUZKw== @@ -24499,6 +24598,15 @@ postcss@^8.4.32: picocolors "^1.0.0" source-map-js "^1.0.2" +postcss@^8.4.36: + version "8.4.38" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" + integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.0" + source-map-js "^1.2.0" + postgres-array@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" @@ -25794,11 +25902,6 @@ requireindex@^1.2.0: resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef" integrity sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww== -requireindex@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.1.0.tgz#e5404b81557ef75db6e49c5a72004893fe03e162" - integrity sha1-5UBLgVV+91225JxacgBIk/4D4WI= - requirejs-config-file@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/requirejs-config-file/-/requirejs-config-file-4.0.0.tgz#4244da5dd1f59874038cc1091d078d620abb6ebc" @@ -26194,13 +26297,6 @@ rollup@^2.70.0: optionalDependencies: fsevents "~2.3.2" -rollup@^3.10.0, rollup@^3.7.0: - version "3.20.2" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.20.2.tgz#f798c600317f216de2e4ad9f4d9ab30a89b690ff" - integrity sha512-3zwkBQl7Ai7MFYQE0y1MeQ15+9jsi7XxfrqwTb/9EK8D9C9+//EBR4M+CuA1KODRaNbFez/lWxA5vhEGZp4MUg== - optionalDependencies: - fsevents "~2.3.2" - rollup@^4.13.0: version "4.13.0" resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.13.0.tgz#dd2ae144b4cdc2ea25420477f68d4937a721237a" @@ -27093,6 +27189,11 @@ source-map-js@^1.1.0: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.1.0.tgz#9e7d5cb46f0689fb6691b30f226937558d0fa94b" integrity sha512-9vC2SfsJzlej6MAaMPLu8HiBSHGdRAJ9hVFYN1ibZoNkeanmDmLUcIrj6G9DGL7XMJ54AKg/G75akXl1/izTOw== +source-map-js@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" + integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== + source-map-loader@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-4.0.0.tgz#bdc6b118bc6c87ee4d8d851f2d4efcc5abdb2ef5" @@ -27426,12 +27527,7 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= -std-env@^3.3.1: - version "3.3.2" - resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.3.2.tgz#af27343b001616015534292178327b202b9ee955" - integrity sha512-uUZI65yrV2Qva5gqE0+A7uVAvO40iPo6jGhs7s8keRfHCmtg+uB2X6EiLGCI9IgL1J17xGhvoOqSz79lzICPTA== - -std-env@^3.5.0: +std-env@^3.3.1, std-env@^3.5.0: version "3.7.0" resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2" integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg== @@ -27766,11 +27862,11 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== strip-literal@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/strip-literal/-/strip-literal-1.0.1.tgz#0115a332710c849b4e46497891fb8d585e404bd2" - integrity sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q== + version "1.3.0" + resolved "https://registry.yarnpkg.com/strip-literal/-/strip-literal-1.3.0.tgz#db3942c2ec1699e6836ad230090b84bb458e3a07" + integrity sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg== dependencies: - acorn "^8.8.2" + acorn "^8.10.0" strip-literal@^2.0.0: version "2.0.0" @@ -28378,25 +28474,20 @@ tiny-warning@^1.0.0: resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== -tinybench@^2.3.1: - version "2.4.0" - resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.4.0.tgz#83f60d9e5545353610fe7993bd783120bc20c7a7" - integrity sha512-iyziEiyFxX4kyxSp+MtY1oCH/lvjH3PxFN8PGCDeqcZWAJ/i+9y+nL85w99PxVzrIvew/GSkSbDYtiGVa85Afg== - -tinybench@^2.5.1: +tinybench@^2.3.1, tinybench@^2.5.1: version "2.6.0" resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.6.0.tgz#1423284ee22de07c91b3752c048d2764714b341b" integrity sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA== -tinypool@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.3.1.tgz#a99c2e446aba9be05d3e1cb756d6aed7af4723b6" - integrity sha512-zLA1ZXlstbU2rlpA4CIeVaqvWq41MTWqLY3FfsAXgC8+f7Pk7zroaJQxDgxn1xNudKW6Kmj4808rPFShUlIRmQ== +tinypool@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.4.0.tgz#3cf3ebd066717f9f837e8d7d31af3c127fdb5446" + integrity sha512-2ksntHOKf893wSAH4z/+JbPpi92esw8Gn9N2deXX+B0EO92hexAVI9GIZZPx7P5aYo5KULfeOSt3kMOmSOy6uA== tinypool@^0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.8.2.tgz#84013b03dc69dacb322563a475d4c0a9be00f82a" - integrity sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ== + version "0.8.3" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.8.3.tgz#e17d0a5315a7d425f875b05f7af653c225492d39" + integrity sha512-Ud7uepAklqRH1bvwy22ynrliC7Dljz7Tm8M/0RBUW+YRa4YHhZ6e4PpgE+fu1zr/WqB1kbeuVrdfeuyIBpy4tw== tinyspy@^1.0.2: version "1.1.1" @@ -28905,11 +28996,6 @@ uc.micro@^1.0.1, uc.micro@^1.0.5: resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== -ufo@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.1.1.tgz#e70265e7152f3aba425bd013d150b2cdf4056d7c" - integrity sha512-MvlCc4GHrmZdAllBc0iUDowff36Q9Ndw/UzqmEKyrfSzokTd9ZCy1i+IIk5hrYKkjoYVQyNbrw7/F8XJ2rEwTg== - ufo@^1.3.2: version "1.5.2" resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.2.tgz#e547561ac56896fc8b9a3f2fb2552169f3629035" @@ -28955,13 +29041,6 @@ underscore@>=1.8.3: resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.12.1.tgz#7bb8cc9b3d397e201cf8553336d262544ead829e" integrity sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw== -undici@^5.21.0: - version "5.26.2" - resolved "https://registry.yarnpkg.com/undici/-/undici-5.26.2.tgz#fa61bfe40f732540d15e58b0c1271872d8e3c995" - integrity sha512-a4PDLQgLTPHVzOK+x3F79/M4GtyYPl+aX9AAK7aQxpwxDwCqkeZCScy7Gk5kWT3JtdFq1uhO3uZJdLtHI4dK9A== - dependencies: - "@fastify/busboy" "^2.0.0" - undici@^5.25.4: version "5.28.3" resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.3.tgz#a731e0eff2c3fcfd41c1169a869062be222d1e5b" @@ -29443,13 +29522,13 @@ v8-to-istanbul@^8.1.0: source-map "^0.7.3" v8-to-istanbul@^9.0.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz#1b83ed4e397f58c85c266a570fc2558b5feb9265" - integrity sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA== + version "9.2.0" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz#2ed7644a245cddd83d4e087b9b33b3e62dfd10ad" + integrity sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA== dependencies: "@jridgewell/trace-mapping" "^0.3.12" "@types/istanbul-lib-coverage" "^2.0.1" - convert-source-map "^1.6.0" + convert-source-map "^2.0.0" validate-npm-package-license@3.0.4, validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.4: version "3.0.4" @@ -29572,10 +29651,10 @@ vfile@^6.0.0: unist-util-stringify-position "^4.0.0" vfile-message "^4.0.0" -vite-node@0.29.2: - version "0.29.2" - resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.29.2.tgz#463626197e248971774075faf3d6896c29cf8062" - integrity sha512-5oe1z6wzI3gkvc4yOBbDBbgpiWiApvuN4P55E8OI131JGrSuo4X3SOZrNmZYo4R8Zkze/dhi572blX0zc+6SdA== +vite-node@0.29.8: + version "0.29.8" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.29.8.tgz#6a1c9d4fb31e7b4e0f825d3a37abe3404e52bd8e" + integrity sha512-b6OtCXfk65L6SElVM20q5G546yu10/kNrhg08afEoWlFRJXFq9/6glsvSVY+aI6YeC1tu2TtAqI2jHEQmOmsFw== dependencies: cac "^6.7.14" debug "^4.3.4" @@ -29595,27 +29674,14 @@ vite-node@1.4.0: picocolors "^1.0.0" vite "^5.0.0" -vite@4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/vite/-/vite-4.0.5.tgz#634f0bd1edf8bb8468ed42a1c3fd938c67d2f94b" - integrity sha512-7m87RC+caiAxG+8j3jObveRLqaWA/neAdCat6JAZwMkSWqFHOvg8MYe5fAQxVBRAuKAQ1S6XDh3CBQuLNbY33w== - dependencies: - esbuild "^0.16.3" - postcss "^8.4.20" - resolve "^1.22.1" - rollup "^3.7.0" - optionalDependencies: - fsevents "~2.3.2" - -"vite@^3.0.0 || ^4.0.0": - version "4.1.4" - resolved "https://registry.yarnpkg.com/vite/-/vite-4.1.4.tgz#170d93bcff97e0ebc09764c053eebe130bfe6ca0" - integrity sha512-3knk/HsbSTKEin43zHu7jTwYWv81f8kgAL99G5NWBcA1LKvtvcVAC4JjBH1arBunO9kQka+1oGbrMKOjk4ZrBg== +vite@4.5.3, "vite@^3.0.0 || ^4.0.0": + version "4.5.3" + resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.3.tgz#d88a4529ea58bae97294c7e2e6f0eab39a50fb1a" + integrity sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg== dependencies: - esbuild "^0.16.14" - postcss "^8.4.21" - resolve "^1.22.1" - rollup "^3.10.0" + esbuild "^0.18.10" + postcss "^8.4.27" + rollup "^3.27.1" optionalDependencies: fsevents "~2.3.2" @@ -29631,13 +29697,13 @@ vite@^4.4.9: fsevents "~2.3.2" vite@^5.0.0: - version "5.1.6" - resolved "https://registry.yarnpkg.com/vite/-/vite-5.1.6.tgz#706dae5fab9e97f57578469eef1405fc483943e4" - integrity sha512-yYIAZs9nVfRJ/AiOLCA91zzhjsHUgMjB+EigzFb6W2XTLO8JixBCKCjvhKZaye+NKYHCrkv3Oh50dH9EdLU2RA== + version "5.2.6" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.6.tgz#fc2ce309e0b4871e938cb0aca3b96c422c01f222" + integrity sha512-FPtnxFlSIKYjZ2eosBQamz4CbyrTizbZ3hnGJlh/wMtCrlp1Hah6AzBLjGI5I2urTfNnpovpHdrL6YRuBOPnCA== dependencies: - esbuild "^0.19.3" - postcss "^8.4.35" - rollup "^4.2.0" + esbuild "^0.20.1" + postcss "^8.4.36" + rollup "^4.13.0" optionalDependencies: fsevents "~2.3.3" @@ -29663,17 +29729,17 @@ vitefu@^0.2.4: integrity sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g== vitest@^0.29.2: - version "0.29.2" - resolved "https://registry.yarnpkg.com/vitest/-/vitest-0.29.2.tgz#0376b547169ddefbde3fbc040b48569ec61d6179" - integrity sha512-ydK9IGbAvoY8wkg29DQ4ivcVviCaUi3ivuPKfZEVddMTenFHUfB8EEDXQV8+RasEk1ACFLgMUqAaDuQ/Nk+mQA== + version "0.29.8" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-0.29.8.tgz#9c13cfa007c3511e86c26e1fe9a686bb4dbaec80" + integrity sha512-JIAVi2GK5cvA6awGpH0HvH/gEG9PZ0a/WoxdiV3PmqK+3CjQMf8c+J/Vhv4mdZ2nRyXFw66sAg6qz7VNkaHfDQ== dependencies: "@types/chai" "^4.3.4" "@types/chai-subset" "^1.3.3" "@types/node" "*" - "@vitest/expect" "0.29.2" - "@vitest/runner" "0.29.2" - "@vitest/spy" "0.29.2" - "@vitest/utils" "0.29.2" + "@vitest/expect" "0.29.8" + "@vitest/runner" "0.29.8" + "@vitest/spy" "0.29.8" + "@vitest/utils" "0.29.8" acorn "^8.8.1" acorn-walk "^8.2.0" cac "^6.7.14" @@ -29686,10 +29752,10 @@ vitest@^0.29.2: std-env "^3.3.1" strip-literal "^1.0.0" tinybench "^2.3.1" - tinypool "^0.3.1" + tinypool "^0.4.0" tinyspy "^1.0.2" vite "^3.0.0 || ^4.0.0" - vite-node "0.29.2" + vite-node "0.29.8" why-is-node-running "^2.2.2" vitest@^1.4.0: @@ -29864,6 +29930,14 @@ watchpack@^2.4.0: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" +watchpack@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.1.tgz#29308f2cac150fa8e4c92f90e0ec954a9fed7fff" + integrity sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + wbuf@^1.1.0, wbuf@^1.7.3: version "1.7.3" resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" @@ -29922,6 +29996,25 @@ webidl-conversions@^7.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== +webpack-bundle-analyzer@^4.5.0: + version "4.10.1" + resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.1.tgz#84b7473b630a7b8c21c741f81d8fe4593208b454" + integrity sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ== + dependencies: + "@discoveryjs/json-ext" "0.5.7" + acorn "^8.0.4" + acorn-walk "^8.0.0" + commander "^7.2.0" + debounce "^1.2.1" + escape-string-regexp "^4.0.0" + gzip-size "^6.0.0" + html-escaper "^2.0.2" + is-plain-object "^5.0.0" + opener "^1.5.2" + picocolors "^1.0.0" + sirv "^2.0.3" + ws "^7.3.1" + webpack-dev-middleware@5.3.3, webpack-dev-middleware@^5.3.1: version "5.3.3" resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz#efae67c2793908e7311f1d9b06f2a08dcc97e51f" @@ -30065,6 +30158,36 @@ webpack@^4.47.0: watchpack "^1.7.4" webpack-sources "^1.4.1" +webpack@^5.76.0: + version "5.91.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.91.0.tgz#ffa92c1c618d18c878f06892bbdc3373c71a01d9" + integrity sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw== + dependencies: + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^1.0.5" + "@webassemblyjs/ast" "^1.12.1" + "@webassemblyjs/wasm-edit" "^1.12.1" + "@webassemblyjs/wasm-parser" "^1.12.1" + acorn "^8.7.1" + acorn-import-assertions "^1.9.0" + browserslist "^4.21.10" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.16.0" + es-module-lexer "^1.2.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.11" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.2.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.3.10" + watchpack "^2.4.1" + webpack-sources "^3.2.3" + webpack@^5.90.3, webpack@~5.90.3: version "5.90.3" resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.90.3.tgz#37b8f74d3ded061ba789bb22b31e82eed75bd9ac" @@ -30351,7 +30474,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", 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.0.1, 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== @@ -30413,6 +30536,11 @@ write-pkg@4.0.0: type-fest "^0.4.1" write-json-file "^3.2.0" +ws@^7.3.1: + version "7.5.9" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" + integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== + ws@^7.4.6: version "7.5.6" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b"